Initial commit

This commit is contained in:
kddd21a
2026-07-29 10:47:40 +00:00
commit 402c3ec6ed
171 changed files with 10536 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
try:
import pymysql
except ImportError:
pymysql = None
if pymysql is not None:
pymysql.install_as_MySQLdb()
+80
View File
@@ -0,0 +1,80 @@
from datetime import datetime, time, timedelta
from django.contrib.auth import get_user_model
from django.db.models import Count
from django.utils import timezone
from exams.models import ExamSet, StudentAttempt
def _chart_points(values, maximum, width=680, height=170, padding=18):
"""Return stable SVG points for a seven-day chart, including an empty state."""
maximum = max(maximum, 1)
usable_width = width - padding * 2
usable_height = height - padding * 2
points = []
for index, value in enumerate(values):
x = padding + usable_width * index / max(len(values) - 1, 1)
y = padding + usable_height - (value / maximum * usable_height)
points.append(f"{x:.1f},{y:.1f}")
return " ".join(points)
def dashboard_callback(request, context):
today = timezone.localdate()
days = [today - timedelta(days=offset) for offset in range(6, -1, -1)]
starts = [timezone.make_aware(datetime.combine(day, time.min)) for day in days]
ends = [start + timedelta(days=1) for start in starts]
attempt_counts = []
active_student_counts = []
for start, end in zip(starts, ends):
attempts = StudentAttempt.objects.filter(started_at__gte=start, started_at__lt=end)
attempt_counts.append(attempts.count())
active_student_counts.append(attempts.values("student_id").distinct().count())
total_attempts = StudentAttempt.objects.count()
seven_day_attempts = sum(attempt_counts)
active_students = StudentAttempt.objects.filter(started_at__gte=starts[0]).values("student_id").distinct().count()
recent_tests = (
ExamSet.objects.annotate(
question_count=Count("sections__questions", distinct=True),
attempt_count=Count("studentattempt", distinct=True),
)
.order_by("-created_at")[:6]
)
chart_maximum = max([*attempt_counts, *active_student_counts, 1])
context.update(
{
"dashboard_metrics": [
{
"label": "Published tests",
"value": ExamSet.objects.filter(is_published=True).count(),
"detail": "Tests available to students",
"icon": "description",
"link": "/admin/exams/examset/?is_published__exact=1",
},
{
"label": "Student attempts",
"value": total_attempts,
"detail": f"{seven_day_attempts} started in the last 7 days",
"icon": "monitoring",
"link": "/admin/exams/studentattempt/",
},
{
"label": "Active students",
"value": active_students,
"detail": "Students active in the last 7 days",
"icon": "group",
"link": "/admin/auth/user/",
},
],
"chart_labels": [f"{day.strftime('%b')} {day.day}" for day in days],
"attempt_points": _chart_points(attempt_counts, chart_maximum),
"active_points": _chart_points(active_student_counts, chart_maximum),
"has_chart_activity": any(attempt_counts),
"recent_tests": recent_tests,
"total_users": get_user_model().objects.count(),
}
)
return context
+16
View File
@@ -0,0 +1,16 @@
"""
ASGI config for core project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
application = get_asgi_application()
+14
View File
@@ -0,0 +1,14 @@
from django.conf import settings
from accounts.access import has_lifetime_premium
def site_settings(request):
return {
"site_support_email": settings.SUPPORT_EMAIL,
"social_facebook_url": settings.SOCIAL_FACEBOOK_URL,
"social_instagram_url": settings.SOCIAL_INSTAGRAM_URL,
"social_youtube_url": settings.SOCIAL_YOUTUBE_URL,
"social_telegram_url": settings.SOCIAL_TELEGRAM_URL,
"has_lifetime_premium": has_lifetime_premium(request.user),
}
+15
View File
@@ -0,0 +1,15 @@
from django import forms
class ContactForm(forms.Form):
name = forms.CharField(max_length=100)
email = forms.EmailField()
subject = forms.CharField(max_length=160)
message = forms.CharField(max_length=4000, widget=forms.Textarea)
website = forms.CharField(required=False, widget=forms.HiddenInput)
def clean_website(self):
value = self.cleaned_data.get("website", "")
if value:
raise forms.ValidationError("Invalid submission.")
return value
+322
View File
@@ -0,0 +1,322 @@
"""
Django settings for core project.
Generated by 'django-admin startproject' using Django 6.0.7.
"""
from pathlib import Path
import os
from django.core.exceptions import ImproperlyConfigured
from django.templatetags.static import static
from django.urls import reverse, reverse_lazy
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
def env_bool(name, default=False):
return os.getenv(name, str(default)).strip().lower() in {"1", "true", "yes", "on"}
def env_list(name, default=""):
return [value.strip() for value in os.getenv(name, default).split(",") if value.strip()]
def load_local_env(path):
"""Load a simple KEY=VALUE .env file without overriding server variables."""
if not path.exists():
return
for raw_line in path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'"))
load_local_env(BASE_DIR / ".env")
# Quick-start development settings - unsuitable for production
DEVELOPMENT_SECRET_KEY = 'django-insecure-development-only-change-me'
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', DEVELOPMENT_SECRET_KEY)
DEBUG = env_bool('DJANGO_DEBUG', True)
ALLOWED_HOSTS = env_list('DJANGO_ALLOWED_HOSTS', '127.0.0.1,localhost')
if not DEBUG and SECRET_KEY == DEVELOPMENT_SECRET_KEY:
raise ImproperlyConfigured("DJANGO_SECRET_KEY must be set to a secure value in production.")
# Application definition
INSTALLED_APPS = [
'unfold',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth.socialaccount.providers.google',
'exams',
'accounts.apps.AccountsConfig',
]
# Keep the back office clean and close to Unfold's maintained defaults.
UNFOLD = {
"SITE_TITLE": "Testpoint Admin",
"SITE_HEADER": "Testpoint",
"SITE_ICON": lambda request: static("images/favicon.svg"),
"SITE_SYMBOL": "menu_book",
"SHOW_HISTORY": True,
"SHOW_VIEW_ON_SITE": True,
"DASHBOARD_CALLBACK": "core.admin_dashboard.dashboard_callback",
"STYLES": [lambda request: "/static/css/unfold-dashboard.css"],
"SIDEBAR": {
"show_search": True,
"show_all_applications": False,
"navigation": [
{
"title": "Navigation",
"separator": True,
"items": [
{"title": "Dashboard", "icon": "dashboard", "link": reverse_lazy("admin:index")},
{"title": "Tests", "icon": "description", "link": reverse_lazy("admin:exams_examset_changelist")},
{"title": "Questions", "icon": "quiz", "link": reverse_lazy("admin:exams_question_changelist")},
{"title": "Results", "icon": "monitoring", "link": reverse_lazy("admin:exams_studentattempt_changelist")},
],
},
{
"title": "Create a test",
"collapsible": True,
"items": [
{"title": "Reading test", "icon": "menu_book", "link": lambda request: reverse("admin:exams_examset_add") + "?category=reading"},
{"title": "Listening test", "icon": "headphones", "link": lambda request: reverse("admin:exams_examset_add") + "?category=listening"},
{"title": "Writing test", "icon": "edit_note", "link": lambda request: reverse("admin:exams_examset_add") + "?category=writing"},
{"title": "Speaking test", "icon": "mic", "link": lambda request: reverse("admin:exams_examset_add") + "?category=speaking"},
{"title": "Full Mock Test", "icon": "assignment", "link": lambda request: reverse("admin:exams_examset_add") + "?category=full"},
],
},
{
"title": "Import content",
"collapsible": True,
"items": [
{
"title": "Import Excel workbook",
"icon": "upload_file",
"link": reverse_lazy("admin:exams_examset_import_excel"),
},
],
},
{
"title": "Users & settings",
"separator": True,
"items": [
{"title": "Users", "icon": "group", "link": reverse_lazy("admin:auth_user_changelist")},
{"title": "Student profiles", "icon": "school", "link": reverse_lazy("admin:accounts_studentprofile_changelist")},
{"title": "Site settings", "icon": "settings", "link": reverse_lazy("admin:sites_site_changelist")},
],
},
],
},
"COLORS": {"primary": {
"50": "oklch(97.2% .016 251)", "100": "oklch(93.8% .043 251)",
"200": "oklch(88.8% .086 251)", "300": "oklch(80.7% .14 251)",
"400": "oklch(70.4% .177 251)", "500": "oklch(60.3% .195 251)",
"600": "oklch(52.7% .19 251)", "700": "oklch(45.2% .16 251)",
"800": "oklch(38.2% .127 251)", "900": "oklch(32.5% .091 251)",
"950": "oklch(23.8% .057 251)"
}},
}
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
]
SITE_ID = 1
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'allauth.account.middleware.AccountMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
if not DEBUG:
MIDDLEWARE.insert(1, 'whitenoise.middleware.WhiteNoiseMiddleware')
ROOT_URLCONF = 'core.urls'
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"core.context_processors.site_settings",
],
},
},
]
# Database
DATABASE_URL = os.getenv('DATABASE_URL')
if DATABASE_URL:
import dj_database_url
database_ssl_default = not DEBUG and DATABASE_URL.startswith(
("postgres://", "postgresql://")
)
DATABASES = {
'default': dj_database_url.parse(
DATABASE_URL,
conn_max_age=600,
conn_health_checks=True,
ssl_require=env_bool('DJANGO_DB_SSL_REQUIRE', database_ssl_default),
)
}
if DATABASES['default']['ENGINE'] == 'django.db.backends.mysql':
mysql_options = DATABASES['default'].setdefault('OPTIONS', {})
mysql_options.setdefault('charset', 'utf8mb4')
mysql_options.setdefault('init_command', "SET sql_mode='STRICT_TRANS_TABLES'")
else:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
AUTH_PASSWORD_VALIDATORS = [
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]
# Internationalization
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'Asia/Tashkent'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
STATIC_URL = '/static/'
# Where collectstatic will put files
STATIC_ROOT = BASE_DIR / 'staticfiles'
# Extra places Django will look for static files (your app-level static folders)
STATICFILES_DIRS = [
BASE_DIR / "static",
]
STORAGES = {
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
"staticfiles": {
"BACKEND": (
"django.contrib.staticfiles.storage.StaticFilesStorage"
if DEBUG
else "whitenoise.storage.CompressedManifestStaticFilesStorage"
),
},
}
# Media files
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
DATA_UPLOAD_MAX_MEMORY_SIZE = 25 * 1024 * 1024
FILE_UPLOAD_MAX_MEMORY_SIZE = 2_621_440
# Authentication
LOGIN_REDIRECT_URL = '/accounts/dashboard/'
ACCOUNT_LOGOUT_REDIRECT_URL = '/'
ACCOUNT_EMAIL_VERIFICATION = os.getenv(
'DJANGO_ACCOUNT_EMAIL_VERIFICATION',
'mandatory' if not DEBUG else 'none',
)
ACCOUNT_UNIQUE_EMAIL = True
ACCOUNT_LOGIN_METHODS = {'email'}
ACCOUNT_SIGNUP_FIELDS = ['email*', 'username*', 'password1*', 'password2*']
EMAIL_BACKEND = os.getenv(
'DJANGO_EMAIL_BACKEND',
(
'django.core.mail.backends.console.EmailBackend'
if DEBUG
else 'django.core.mail.backends.smtp.EmailBackend'
),
)
EMAIL_HOST = os.getenv('DJANGO_EMAIL_HOST', 'localhost')
EMAIL_PORT = int(os.getenv('DJANGO_EMAIL_PORT', '587'))
EMAIL_HOST_USER = os.getenv('DJANGO_EMAIL_HOST_USER', '')
EMAIL_HOST_PASSWORD = os.getenv('DJANGO_EMAIL_HOST_PASSWORD', '')
EMAIL_USE_TLS = env_bool('DJANGO_EMAIL_USE_TLS', True)
DEFAULT_FROM_EMAIL = os.getenv('DJANGO_DEFAULT_FROM_EMAIL', 'Testpoint <noreply@localhost>')
SERVER_EMAIL = os.getenv('DJANGO_SERVER_EMAIL', DEFAULT_FROM_EMAIL)
SUPPORT_EMAIL = os.getenv('DJANGO_SUPPORT_EMAIL', 'support@ieltsmock.com')
SOCIAL_FACEBOOK_URL = os.getenv('SOCIAL_FACEBOOK_URL', '')
SOCIAL_INSTAGRAM_URL = os.getenv('SOCIAL_INSTAGRAM_URL', '')
SOCIAL_YOUTUBE_URL = os.getenv('SOCIAL_YOUTUBE_URL', '')
SOCIAL_TELEGRAM_URL = os.getenv('SOCIAL_TELEGRAM_URL', '')
SOCIALACCOUNT_PROVIDERS = {
'google': {
'SCOPE': ['profile', 'email'],
'AUTH_PARAMS': {'access_type': 'online'},
}
}
# Production security. These remain development-friendly while DEBUG=True.
CSRF_TRUSTED_ORIGINS = env_list('DJANGO_CSRF_TRUSTED_ORIGINS')
SECURE_SSL_REDIRECT = env_bool('DJANGO_SECURE_SSL_REDIRECT', not DEBUG)
SESSION_COOKIE_SECURE = not DEBUG
CSRF_COOKIE_SECURE = not DEBUG
SECURE_HSTS_SECONDS = int(os.getenv('DJANGO_SECURE_HSTS_SECONDS', '3600' if not DEBUG else '0'))
SECURE_HSTS_INCLUDE_SUBDOMAINS = env_bool('DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS', False)
SECURE_HSTS_PRELOAD = env_bool('DJANGO_SECURE_HSTS_PRELOAD', False)
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
if env_bool('DJANGO_TRUST_PROXY_HEADERS', False):
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
LOG_LEVEL = os.getenv('DJANGO_LOG_LEVEL', 'INFO')
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'standard': {
'format': '{asctime} {levelname} {name}: {message}',
'style': '{',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'standard',
},
},
'root': {'handlers': ['console'], 'level': LOG_LEVEL},
'loggers': {
'django.request': {
'handlers': ['console'],
'level': 'WARNING',
'propagate': False,
},
},
}
+150
View File
@@ -0,0 +1,150 @@
from django.contrib.auth.models import User
from django.test import TestCase
from django.test import override_settings
from django.core import mail
class PublicNavigationTests(TestCase):
def test_public_navigation_has_working_destinations_and_mobile_bundle(self):
response = self.client.get("/")
self.assertContains(response, 'href="/#about"')
self.assertContains(response, 'href="/exams/"')
self.assertContains(response, 'href="/accounts/login/"')
self.assertContains(response, 'href="/accounts/signup/"')
self.assertContains(response, "bootstrap.bundle.min.js")
self.assertContains(response, 'id="mainNavigation"')
self.assertContains(response, "footer.css?v=20260727.2")
self.assertNotContains(response, 'href="#"')
def test_authenticated_navigation_shows_dashboard_and_logout(self):
user = User.objects.create_user(username="nav-user", password="test-pass-123")
self.client.force_login(user)
response = self.client.get("/")
self.assertContains(response, 'href="/accounts/dashboard/"')
self.assertContains(response, 'href="/accounts/logout/"')
self.assertNotContains(response, 'href="/accounts/login/"')
def test_admin_dashboard_uses_real_unfold_dashboard_shell(self):
admin_user = User.objects.create_superuser(
username="dashboard-admin",
email="dashboard@example.com",
password="test-pass-123",
)
self.client.force_login(admin_user)
response = self.client.get("/admin/")
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Activity overview")
self.assertContains(response, "Published tests")
self.assertContains(response, "No activity yet")
self.assertContains(response, "/static/unfold/css/styles.css")
def test_navigation_marks_only_the_current_main_page_active(self):
home_response = self.client.get("/")
self.assertContains(home_response, 'class="nav-link active" href="/"')
user = User.objects.create_user(username="exam-nav", password="test-pass-123")
self.client.force_login(user)
exams_response = self.client.get("/exams/")
self.assertContains(exams_response, 'class="nav-link active" href="/exams/"')
self.assertNotContains(exams_response, 'class="nav-link active" href="/"')
def test_public_information_pages_resolve(self):
faq_response = self.client.get("/faq/")
privacy_response = self.client.get("/privacy/")
terms_response = self.client.get("/terms/")
self.assertEqual(faq_response.status_code, 200)
self.assertContains(faq_response, "Frequently Asked Questions")
self.assertEqual(privacy_response.status_code, 200)
self.assertContains(privacy_response, "Privacy Policy")
self.assertEqual(terms_response.status_code, 200)
self.assertContains(terms_response, "Terms of Use")
self.assertContains(terms_response, "not an official IELTS examination service")
def test_footer_uses_configured_links_without_fake_social_destinations(self):
response = self.client.get("/")
self.assertContains(response, "&copy;", html=False)
self.assertContains(response, "support@ieltsmock.com")
self.assertNotContains(response, "https://www.facebook.com/")
self.assertNotContains(response, "https://www.instagram.com/")
def test_account_pages_use_ielts_mock_branding(self):
login_response = self.client.get("/accounts/login/")
signup_response = self.client.get("/accounts/signup/")
reset_response = self.client.get("/accounts/password/reset/")
self.assertContains(login_response, "Welcome back")
self.assertNotContains(login_response, "SpcHub")
self.assertContains(signup_response, "Create your account")
self.assertContains(reset_response, "Reset your password")
def test_account_recovery_confirmation_screen_is_branded(self):
response = self.client.post(
"/accounts/password/reset/",
{"email": "missing@example.com"},
follow=True,
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Check your email")
self.assertContains(response, "Testpoint")
def test_health_endpoint_reports_database_readiness(self):
response = self.client.get("/health/")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"status": "ok", "database": "ok"})
def test_missing_page_uses_branded_error_template(self):
response = self.client.get("/this-page-does-not-exist/")
self.assertEqual(response.status_code, 404)
self.assertContains(response, "Page not found", status_code=404)
def test_home_has_search_and_accessibility_metadata(self):
response = self.client.get("/")
self.assertContains(response, 'rel="canonical"')
self.assertContains(response, 'property="og:title"')
self.assertContains(response, 'href="/static/images/favicon.svg"')
self.assertContains(response, "Skip to main content")
self.assertContains(response, 'id="main-content"')
def test_robots_and_sitemap_resolve(self):
robots_response = self.client.get("/robots.txt")
sitemap_response = self.client.get("/sitemap.xml")
self.assertEqual(robots_response.status_code, 200)
self.assertContains(robots_response, "Disallow: /admin/")
self.assertContains(robots_response, "/sitemap.xml")
self.assertEqual(sitemap_response.status_code, 200)
self.assertEqual(sitemap_response["Content-Type"], "application/xml")
self.assertContains(sitemap_response, "/faq/")
self.assertContains(sitemap_response, "/terms/")
@override_settings(
EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend",
SUPPORT_EMAIL="support@example.com",
)
def test_contact_form_sends_support_email(self):
response = self.client.post(
"/contact/",
{
"name": "Test Student",
"email": "student@example.com",
"subject": "Account help",
"message": "Please help with my account.",
"website": "",
},
)
self.assertRedirects(response, "/contact/")
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].to, ["support@example.com"])
self.assertEqual(mail.outbox[0].reply_to, ["student@example.com"])
+25
View File
@@ -0,0 +1,25 @@
from django.contrib import admin
from django.conf import settings
from django.conf.urls.static import static
from django.urls import path, include
from .views import contact, faq, health, home, privacy, robots, sitemap, terms
urlpatterns = [
path('admin/', admin.site.urls),
path('', home, name='home'),
path('faq/', faq, name='faq'),
path('privacy/', privacy, name='privacy'),
path('terms/', terms, name='terms'),
path('contact/', contact, name='contact'),
path('health/', health, name='health'),
path('robots.txt', robots, name='robots'),
path('sitemap.xml', sitemap, name='sitemap'),
path('accounts/', include('accounts.urls')),
path('exams/', include('exams.urls')),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+117
View File
@@ -0,0 +1,117 @@
import logging
from django.conf import settings
from django.contrib import messages
from django.core.mail import EmailMessage
from django.db import connection
from django.http import HttpResponse, JsonResponse
from django.shortcuts import redirect, render
from django.urls import reverse
from .forms import ContactForm
from accounts.leaderboard import build_leaderboard
logger = logging.getLogger(__name__)
def home(request):
rows, current_row, _ = build_leaderboard(
period="week",
current_user=request.user if request.user.is_authenticated else None,
)
preview_user = current_row or {
"rank": 24,
"name": "Akhmed",
"band": 7.5,
"trend": "up",
"initial": "A",
"is_current": True,
"demo": True,
}
return render(
request,
"home.html",
{"leaderboard_preview": rows[:5], "leaderboard_preview_user": preview_user},
)
def faq(request):
return render(request, "faq.html")
def privacy(request):
return render(request, "privacy.html")
def terms(request):
return render(request, "terms.html")
def contact(request):
form = ContactForm(request.POST or None)
if request.method == "POST" and form.is_valid():
body = (
f"Name: {form.cleaned_data['name']}\n"
f"Email: {form.cleaned_data['email']}\n\n"
f"{form.cleaned_data['message']}"
)
email = EmailMessage(
subject=f"Testpoint support: {form.cleaned_data['subject']}",
body=body,
from_email=settings.DEFAULT_FROM_EMAIL,
to=[settings.SUPPORT_EMAIL],
reply_to=[form.cleaned_data["email"]],
)
try:
email.send(fail_silently=False)
except Exception:
logger.exception("Support contact email could not be sent")
messages.error(
request,
"We could not send your message. Please try again shortly.",
)
else:
messages.success(
request,
"Your message has been sent. We'll reply as soon as possible.",
)
return redirect("contact")
return render(
request,
"contact.html",
{"form": form, "support_email": settings.SUPPORT_EMAIL},
)
def health(request):
"""Lightweight process and database readiness check for the host."""
try:
connection.ensure_connection()
except Exception:
return JsonResponse(
{"status": "unhealthy", "database": "unavailable"}, status=503
)
return JsonResponse({"status": "ok", "database": "ok"})
def robots(request):
sitemap_url = request.build_absolute_uri(reverse("sitemap"))
body = f"User-agent: *\nAllow: /\nDisallow: /admin/\nSitemap: {sitemap_url}\n"
return HttpResponse(body, content_type="text/plain")
def sitemap(request):
urls = [
request.build_absolute_uri(reverse("home")),
request.build_absolute_uri(reverse("faq")),
request.build_absolute_uri(reverse("privacy")),
request.build_absolute_uri(reverse("terms")),
request.build_absolute_uri(reverse("contact")),
]
return render(
request,
"sitemap.xml",
{"urls": urls},
content_type="application/xml",
)
+16
View File
@@ -0,0 +1,16 @@
"""
WSGI config for core project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
application = get_wsgi_application()