Skip to content

Commit

Permalink
Good path verification flow done and done.
Browse files Browse the repository at this point in the history
  • Loading branch information
davepeck committed Apr 18, 2024
1 parent 19b682b commit 8e4511a
Show file tree
Hide file tree
Showing 13 changed files with 668 additions and 80 deletions.
195 changes: 189 additions & 6 deletions server/vb/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,23 @@
from django import forms
from django.contrib import admin
from django.core.files.uploadedfile import UploadedFile
from django.db import models
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.safestring import mark_safe
from django.utils.timezone import now as django_now

from server.admin import admin_site

from .models import Contest, GiftCard, ImageMimeType, Logo, School, Student
from .models import (
Contest,
EmailValidationLink,
GiftCard,
ImageMimeType,
Logo,
School,
Student,
)


def validate_file_is_image(file: UploadedFile) -> None:
Expand Down Expand Up @@ -88,6 +99,7 @@ class SchoolAdmin(admin.ModelAdmin, RenderLogoSpecimenMixin):
"slug_display",
"logo_display",
"active_contest",
"student_count",
"mascot",
"mail_domains",
)
Expand All @@ -112,14 +124,96 @@ def slug_display(self, obj: School):
def active_contest(self, obj: School):
"""Return whether the school has an active contest."""
current = obj.contests.current()
return "" if current is None else current.name
if current is None:
return ""
url = reverse("admin:vb_contest_change", args=[current.pk])
return mark_safe(f'<a href="{url}">{current.name}</a>')

@admin.display(description="Students")
def student_count(self, obj: School):
"""Return the number of students at the school."""
count = obj.students.count()
return count if count > 0 else ""


class EmailValidatedListFilter(admin.SimpleListFilter):
"""Email validated list filter."""

title = "Email Validated"
parameter_name = "email_validated"

def lookups(self, request, model_admin):
"""Return the email validated lookups."""
return (
("yes", "Yes"),
("no", "No"),
)

def queryset(self, request, queryset):
"""Filter the queryset by email validated."""
if self.value() == "yes":
return queryset.filter(email_validated_at__isnull=False)
if self.value() == "no":
return queryset.filter(email_validated_at__isnull=True)
return queryset


class StudentAdmin(admin.ModelAdmin):
"""Student admin."""

list_display = ("school", "email", "first_name", "last_name")
list_display = (
"name",
"show_school",
"email",
"show_is_validated",
"gift_card_total",
)
search_fields = ("school__name", "email", "first_name", "last_name")
list_filter = (EmailValidatedListFilter, "school__name")

@admin.display(description="School")
def show_school(self, obj: Student) -> str:
"""Return the student's school."""
# Get the link to the school admin page.
school_admin_link = reverse("admin:vb_school_change", args=[obj.school.pk])
return mark_safe(f'<a href="{school_admin_link}">{obj.school.name}</a>')

@admin.display(description="Email Validated", boolean=True)
def show_is_validated(self, obj: Student) -> bool:
"""Return whether the student's email is validated."""
return obj.is_validated

@admin.display(description="Gift Card Total")
def gift_card_total(self, obj: Student) -> str | None:
"""Return the total number of gift cards the student has received."""
usd = obj.gift_cards.aggregate(total=models.Sum("amount"))["total"] or 0
return f"${usd}" if usd > 0 else None


class StatusListFilter(admin.SimpleListFilter):
"""Status list filter."""

title = "Status"
parameter_name = "status"

def lookups(self, request, model_admin):
"""Return the status lookups."""
return (
("ongoing", "Ongoing"),
("upcoming", "Upcoming"),
("past", "Past"),
)

def queryset(self, request, queryset):
"""Filter the queryset by status."""
when = django_now()
if self.value() == "ongoing":
return queryset.filter(start_at__lte=when, end_at__gt=when)
if self.value() == "upcoming":
return queryset.filter(start_at__gt=when)
if self.value() == "past":
return queryset.filter(end_at__lte=when)
return queryset


class ContestAdmin(admin.ModelAdmin):
Expand All @@ -128,21 +222,110 @@ class ContestAdmin(admin.ModelAdmin):
list_display = (
"id",
"name",
"school",
"status",
"show_school",
"start_at",
"end_at",
)
search_fields = ("school__name", "school__short_name", "school__slug")
list_filter = (StatusListFilter, "school__name")

@admin.display(description="Status")
def status(self, obj: Contest) -> str:
"""Return the contest's status."""
if obj.is_ongoing():
return "Ongoing"
elif obj.is_upcoming():
return "Upcoming"
elif obj.is_past():
return "Past"
raise ValueError("Invalid contest status")

@admin.display(description="School")
def show_school(self, obj: Contest) -> str:
"""Return the student's school."""
# Get the link to the school admin page.
school_admin_link = reverse("admin:vb_school_change", args=[obj.school.pk])
return mark_safe(f'<a href="{school_admin_link}">{obj.school.name}</a>')


class GiftCardAdmin(admin.ModelAdmin):
"""Gift card admin."""

list_display = ("id", "created_at")
search_fields = ("id", "created_at")
list_display = (
"id",
"created_at",
"show_amount",
"show_student",
"show_school",
"show_contest",
)
search_fields = ("id", "created_at", "student__email", "student__name")

@admin.display(description="Amount")
def show_amount(self, obj: GiftCard) -> str:
"""Return the gift card's amount."""
return f"${obj.amount}"

@admin.display(description="Student")
def show_student(self, obj: GiftCard) -> str:
"""Return the gift card's student."""
url = reverse("admin:vb_student_change", args=[obj.student.pk])
return mark_safe(f'<a href="{url}">{obj.student.name}</a>')

@admin.display(description="School")
def show_school(self, obj: GiftCard) -> str:
"""Return the gift card's school."""
url = reverse("admin:vb_school_change", args=[obj.student.school.pk])
return mark_safe(f'<a href="{url}">{obj.student.school.name}</a>')

@admin.display(description="Contest")
def show_contest(self, obj: GiftCard) -> str:
"""Return the gift card's contest."""
url = reverse("admin:vb_contest_change", args=[obj.contest.pk])
return mark_safe(f'<a href="{url}">{obj.contest.name}</a>')


class EmailValidationLinkAdmin(admin.ModelAdmin):
"""Email validation link admin."""

list_display = (
"id",
"email",
"show_student",
"show_school",
"show_contest",
"token",
"is_consumed",
)
search_fields = ("email", "token")

@admin.display(description="Student")
def show_student(self, obj: GiftCard) -> str:
"""Return the gift card's student."""
url = reverse("admin:vb_student_change", args=[obj.student.pk])
return mark_safe(f'<a href="{url}">{obj.student.name}</a>')

@admin.display(description="School")
def show_school(self, obj: GiftCard) -> str:
"""Return the gift card's school."""
url = reverse("admin:vb_school_change", args=[obj.student.school.pk])
return mark_safe(f'<a href="{url}">{obj.student.school.name}</a>')

@admin.display(description="Contest")
def show_contest(self, obj: GiftCard) -> str:
"""Return the gift card's contest."""
url = reverse("admin:vb_contest_change", args=[obj.contest.pk])
return mark_safe(f'<a href="{url}">{obj.contest.name}</a>')

@admin.display(description="Is Consumed", boolean=True)
def is_consumed(self, obj: EmailValidationLink) -> bool:
"""Return whether the email validation link is consumed."""
return obj.is_consumed()


admin_site.register(School, SchoolAdmin)
admin_site.register(Student, StudentAdmin)
admin_site.register(Contest, ContestAdmin)
admin_site.register(GiftCard, GiftCardAdmin)
admin_site.register(EmailValidationLink, EmailValidationLinkAdmin)
40 changes: 40 additions & 0 deletions server/vb/migrations/0002_email_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Generated by Django 5.0.3 on 2024-04-18 19:32

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('vb', '0001_initial'),
]

operations = [
migrations.AlterField(
model_name='student',
name='email_validated_at',
field=models.DateTimeField(blank=True, db_index=True, default=None, help_text='The time the email was validated.', null=True),
),
migrations.AlterField(
model_name='student',
name='other_emails',
field=models.JSONField(blank=True, default=list, help_text='The second+ emails for this user. These may not be validated.'),
),
migrations.CreateModel(
name='EmailValidationLink',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(help_text='The specific email address to be validated.', max_length=254)),
('token', models.CharField(help_text='The current validation token, if any.', max_length=255, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True, help_text='The time the email validation link was most recently created.')),
('consumed_at', models.DateTimeField(blank=True, default=None, help_text='The time the email validation link was first consumed.', null=True)),
('contest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='email_validation_links', to='vb.contest')),
('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='email_validation_links', to='vb.student')),
],
),
migrations.AddConstraint(
model_name='emailvalidationlink',
constraint=models.UniqueConstraint(fields=('student', 'contest'), name='unique_student_contest_email_validation_link'),
),
]
Loading

0 comments on commit 8e4511a

Please sign in to comment.