Skip to content

Commit

Permalink
Thread through no-constest behavior.
Browse files Browse the repository at this point in the history
  • Loading branch information
davepeck committed Apr 19, 2024
1 parent b98388b commit 402ea45
Show file tree
Hide file tree
Showing 13 changed files with 121 additions and 122 deletions.
8 changes: 4 additions & 4 deletions server/vb/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,11 @@ def slug_display(self, obj: School):
@admin.display(description="active contest")
def active_contest(self, obj: School):
"""Return whether the school has an active contest."""
current = obj.contests.current()
if current is None:
current_contest = obj.contests.current()
if current_contest is None:
return ""
url = reverse("admin:vb_contest_change", args=[current.pk])
return mark_safe(f'<a href="{url}">{current.name}</a>')
url = reverse("admin:vb_contest_change", args=[current_contest.pk])
return mark_safe(f'<a href="{url}">{current_contest.name}</a>')

@admin.display(description="Students")
def student_count(self, obj: School):
Expand Down
21 changes: 16 additions & 5 deletions server/vb/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 5.0.3 on 2024-04-17 00:39
# Generated by Django 5.0.3 on 2024-04-19 18:59

import django.core.validators
import django.db.models.deletion
Expand Down Expand Up @@ -44,8 +44,6 @@ class Migration(migrations.Migration):
('start_at', models.DateTimeField()),
('end_at', models.DateTimeField()),
('amount', models.IntegerField(default=5, help_text='The USD amount of the gift card.')),
('name_template', models.TextField(default='${{ contest.amount }} Amazon Gift Card Giveaway', help_text='The name of the contest. Can use template variables like {{ school.name }} and {{ contest.amount }}.', max_length=255)),
('description_template', models.TextField(default='{{ school.short_name }} students: check your voter registration to win a ${{ contest.amount }} Amazon gift card.', help_text='A description of the contest. Can use template variables like {{ school.name }} and {{ contest.amount }}.')),
('school', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contests', to='vb.school')),
],
),
Expand All @@ -57,8 +55,8 @@ class Migration(migrations.Migration):
('updated_at', models.DateTimeField(auto_now=True)),
('email', models.EmailField(help_text='The first email address we ever saw for this user.', max_length=254, unique=True)),
('hash', models.CharField(help_text='A unique ID for the student derived from their email address.', max_length=64, unique=True)),
('other_emails', models.JSONField(blank=True, default=list, help_text='The second (and beyond) emails for this user.')),
('email_validated_at', models.DateTimeField(blank=True, default=None, help_text='The time the email was validated.', null=True)),
('other_emails', models.JSONField(blank=True, default=list, help_text='The second+ emails for this user. These may not be validated.')),
('email_validated_at', models.DateTimeField(blank=True, db_index=True, default=None, help_text='The time the email was validated.', null=True)),
('first_name', models.CharField(max_length=255)),
('last_name', models.CharField(max_length=255)),
('phone', models.CharField(blank=True, default='', max_length=255)),
Expand All @@ -77,6 +75,19 @@ class Migration(migrations.Migration):
('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gift_cards', to='vb.student')),
],
),
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(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='email_validation_links', to='vb.contest')),
('school', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='email_validation_links', to='vb.school')),
('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='email_validation_links', to='vb.student')),
],
),
migrations.AddConstraint(
model_name='giftcard',
constraint=models.UniqueConstraint(fields=('student', 'contest'), name='unique_student_contest_gift_card'),
Expand Down
40 changes: 0 additions & 40 deletions server/vb/migrations/0002_email_validation.py

This file was deleted.

56 changes: 23 additions & 33 deletions server/vb/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class School(models.Model):
logo: "Logo"
contests: "ContestManager"
students: "StudentManager"
email_validation_links: "EmailValidationLinkManager"

def normalize_email(self, address: str) -> str:
"""Normalize an email address for this school."""
Expand Down Expand Up @@ -143,6 +144,12 @@ def past(self, when: datetime.datetime | None = None):
when = when or django_now()
return self.get_queryset().filter(end_at__lte=when)

def most_recent_past(
self, when: datetime.datetime | None = None
) -> "Contest | None":
"""Return the single most recent past contest, if any."""
return self.past(when).order_by("-end_at").first()

def current(self, when: datetime.datetime | None = None) -> "Contest | None":
"""Return the single current contest."""
return self.ongoing(when).first()
Expand Down Expand Up @@ -171,33 +178,21 @@ class Contest(models.Model):
blank=False, help_text="The USD amount of the gift card.", default=5
)

# The contest name and description can be templated.
name_template = models.TextField(
blank=False,
max_length=255,
help_text="The name of the contest. Can use template variables like {{ school.name }} and {{ contest.amount }}.", # noqa
default="${{ contest.amount }} Amazon Gift Card Giveaway",
)

gift_cards: "GiftCardManager"

@property
def name(self) -> str:
"""Render the contest name template."""
template_str = "${{ contest.amount }} Amazon Gift Card Giveaway"
context = {"school": self.school, "contest": self}
return Template(self.name_template).render(Context(context))

description_template = models.TextField(
blank=False,
help_text="A description of the contest. Can use template variables like {{ school.name }} and {{ contest.amount }}.", # noqa
default="{{ school.short_name }} students: check your voter registration to win a ${{ contest.amount }} Amazon gift card.", # noqa
)
return Template(template_str).render(Context(context))

@property
def description(self) -> str:
"""Render the contest description template."""
template_str = "{{ school.short_name }} students: check your voter registration to win a ${{ contest.amount }} Amazon gift card." # noqa
context = {"school": self.school, "contest": self}
return Template(self.description_template).render(Context(context))
return Template(template_str).render(Context(context))

def is_upcoming(self, when: datetime.datetime | None = None) -> bool:
"""Return whether the contest is upcoming."""
Expand Down Expand Up @@ -327,8 +322,18 @@ class EmailValidationLink(models.Model):
student = models.ForeignKey(
Student, on_delete=models.CASCADE, related_name="email_validation_links"
)
school = models.ForeignKey(
School,
on_delete=models.CASCADE,
related_name="email_validation_links",
)
contest = models.ForeignKey(
Contest, on_delete=models.CASCADE, related_name="email_validation_links"
Contest,
on_delete=models.CASCADE,
related_name="email_validation_links",
null=True,
# A user may still check registration outside of a contest.
# CONSTRAINT: contest.school must == student.school
)

email = models.EmailField(
Expand All @@ -353,15 +358,10 @@ class EmailValidationLink(models.Model):
help_text="The time the email validation link was first consumed.",
)

@property
def school(self) -> School:
"""Return the school associated with the email validation link."""
return self.student.school

@property
def relative_url(self) -> str:
"""Return the relative URL for the email validation link."""
return reverse("vb:validate_email", args=[self.contest.school.slug, self.token])
return reverse("vb:validate_email", args=[self.school.slug, self.token])

@property
def absolute_url(self) -> str:
Expand All @@ -381,16 +381,6 @@ def consume(self, when: datetime.datetime | None = None) -> None:
# Demeter says no, but my heart says yes.
self.student.mark_validated(when)

class Meta:
"""Define the email validation link model's meta options."""

constraints = [
models.UniqueConstraint(
fields=["student", "contest"],
name="unique_student_contest_email_validation_link",
)
]


class GiftCardManager(models.Manager):
"""A custom manager for the gift card model."""
Expand Down
14 changes: 11 additions & 3 deletions server/vb/ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,20 @@ def get_or_create_student(


def send_validation_link_email(
student: Student, contest: Contest, email: str
student: Student, school: School, contest: Contest | None, email: str
) -> EmailValidationLink:
"""Generate a validation link to a student for a contest."""
link = EmailValidationLink.objects.create(
student=student, contest=contest, email=email, token=make_token(12)
student=student,
school=school,
contest=contest,
email=email,
token=make_token(12),
)
if contest:
button_text = f"Get my ${contest.amount} gift card"
else:
button_text = "Validate my email"
success = send_template_email(
to=email,
template_base="email/validate",
Expand All @@ -137,7 +145,7 @@ def send_validation_link_email(
"contest": contest,
"email": email,
"link": link,
"button_text": f"Get my ${contest.amount} gift card",
"button_text": button_text,
},
)
if not success:
Expand Down
2 changes: 2 additions & 0 deletions server/vb/templates/check.dhtml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@
alt="{{ school.short_name }} {{ school.mascot }} logo" />
{% if current_contest %}
{% include "components/countdown.dhtml" with contest=current_contest %}
{% else %}
<p>Check your voter registration status below.</p>
{% endif %}
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion server/vb/templates/email/validate/body.dhtml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Hi {{ student.first_name }},
{% include "email/base/p_close.html" %}
{% include "email/base/p.html" %}
Thanks for checking your voter registration status! Click this button to get your gift card.
Thanks for checking your voter registration status! Click this button to {% if contest %}get your gift card{% else %}validate your email address{% endif %}.
{% include "email/base/p_close.html" %}
{% include "email/base/button.html" with url=link.relative_url title=button_text %}
{% endblock message %}
2 changes: 1 addition & 1 deletion server/vb/templates/email/validate/body.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

Thanks for checking your voter registration status! You're almost done.

Click this link to get your ${{ contest.amount }} Amazon gift card:
{% if contest %}Click this link to get your ${{ contest.amount }} Amazon gift card:{% else %}Click this link to validate your email address:{% endif %}

{{ link.absolute_url }}
{% endblock message %}
2 changes: 1 addition & 1 deletion server/vb/templates/email/validate/subject.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Your VoterBowl gift card awaits
{% if contest %}Your VoterBowl gift card awaits{% else %}Verify your email address{% endif %}
7 changes: 6 additions & 1 deletion server/vb/templates/finish_check.dhtml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,10 @@
setTimeout(() => fireworks.stop(), 10_000);
})(me());
</script>
<b>Please check your email</b>. We've sent you a link to claim your ${{ current_contest.amount }} gift card.
<b>Please check your email</b>.
{% if current_contest %}
We've sent you a link to claim your ${{ current_contest.amount }} gift card.
{% else %}
We've sent you a validation link to confirm your address.
{% endif %}
</p>
8 changes: 7 additions & 1 deletion server/vb/templates/school.dhtml
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,14 @@
<h2>Welcome to the Voter Bowl</h2>
{% if current_contest %}
<p>{{ current_contest.description }}</p>
{% elif past_contest %}
<p>
{{ school.short_name }} students: the ${{ past_contest.amount }} giveaway ended {{ past_contest.ended_at | timesince }}, but it's always a good time to make sure you're ready to vote.
</p>
{% else %}
<p>There is no contest at this time. TODO: show something useful in this state?</p>
<p>
{{ school.short_name }} students: there's no giveaway right now, but it's always a good time to make sure you're ready to vote.
</p>
{% endif %}
<div class="button-holder">
{% include "components/button.dhtml" with text="Check my voter status" href="./check/" bg_color=school.logo.action_color color=school.logo.action_text_color %}
Expand Down
22 changes: 13 additions & 9 deletions server/vb/templates/verify_email.dhtml
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,19 @@
<div class="container">
<img src="{{ school.logo.url }}"
alt="{{ school.short_name }} {{ school.mascot }} logo" />
<p>Congrats! You've got a ${{ gift_card.amount }} Amazon gift card!</p>
<h2>
<span class="code">{{ claim_code }}</span>
<span class="clipboard" title="Copy to clipboard">{% include "clipboard.svg" %}</span>
<span class="copied hidden" title="Copied!">{% include "clipboard_check.svg" %}</span>
</h2>
<p>
To use your gift card, copy the code above and paste it into <a href="https://www.amazon.com/gc/redeem" target="_blank">Amazon.com's redemption page</a>. That's all there is to it.
</p>
{% if gift_card and claim_code %}
<p>Congrats! You've got a ${{ gift_card.amount }} Amazon gift card!</p>
<h2>
<span class="code">{{ claim_code }}</span>
<span class="clipboard" title="Copy to clipboard">{% include "clipboard.svg" %}</span>
<span class="copied hidden" title="Copied!">{% include "clipboard_check.svg" %}</span>
</h2>
<p>
To use your gift card, copy the code above and paste it into <a href="https://www.amazon.com/gc/redeem" target="_blank">Amazon.com's redemption page</a>. That's all there is to it.
</p>
{% else %}
<p>Thanks for verifying your email address, and for checking your registration status. Happy voting in 2024!</p>
{% endif %}
</div>
</main>
<div class="faq">
Expand Down
Loading

0 comments on commit 402ea45

Please sign in to comment.