diff --git a/home/management/commands/applications_open_email.py b/home/management/commands/applications_open_email.py index 59315be8..87f4a69c 100644 --- a/home/management/commands/applications_open_email.py +++ b/home/management/commands/applications_open_email.py @@ -48,7 +48,7 @@ def handle(self, *args, **options): "application_end_date": applications_starting_today.application_end_date.strftime( "%b %d, %Y" ), - "cta_link": applications_starting_today.application_url, + "cta_link": applications_starting_today.get_application_url(), "name": user.get_full_name(), "unsubscribe_link": user.profile.create_unsubscribe_link(), } diff --git a/home/managers.py b/home/managers.py index 220a2851..7f03c569 100644 --- a/home/managers.py +++ b/home/managers.py @@ -1,5 +1,8 @@ from __future__ import annotations +from django.db.models import Exists +from django.db.models import OuterRef +from django.db.models import Value from django.db.models.query import QuerySet from django.utils import timezone @@ -27,6 +30,21 @@ def past(self): return self.filter(start_time__lte=timezone.now()) +class SessionQuerySet(QuerySet): + def with_applications(self, user): + from home.models import UserSurveyResponse + + if user.is_anonymous: + return self.annotate(completed_application=Value(False)) + return self.annotate( + completed_application=Exists( + UserSurveyResponse.objects.filter( + survey_id=OuterRef("application_survey_id"), user_id=user.id + ) + ) + ) + + class SessionMembershipQuerySet(QuerySet): def _SessionMembership(self): return self.model.session._meta.model diff --git a/home/migrations/0026_session_application_survey_and_more.py b/home/migrations/0026_session_application_survey_and_more.py new file mode 100644 index 00000000..34139242 --- /dev/null +++ b/home/migrations/0026_session_application_survey_and_more.py @@ -0,0 +1,35 @@ +# Generated by Django 4.1.13 on 2024-03-27 17:27 +from __future__ import annotations + +import django.db.models.deletion +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("home", "0025_alter_generalpage_content"), + ] + + operations = [ + migrations.AddField( + model_name="session", + name="application_survey", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="application_sessions", + to="home.survey", + ), + ), + migrations.AlterField( + model_name="session", + name="application_url", + field=models.URLField( + blank=True, + help_text="This is a URL to the Djangonaut application form. Likely Google Forms.", + null=True, + ), + ), + ] diff --git a/home/models/session.py b/home/models/session.py index 2286de7f..550507e9 100644 --- a/home/models/session.py +++ b/home/models/session.py @@ -9,6 +9,7 @@ from django.utils.translation import gettext_lazy as _ from home.managers import SessionMembershipQuerySet +from home.managers import SessionQuerySet class Session(models.Model): @@ -39,10 +40,21 @@ class Session(models.Model): application_end_date = models.DateField( help_text="This is the end date for Djangonaut applications." ) + application_survey = models.ForeignKey( + "home.Survey", + related_name="application_sessions", + on_delete=models.SET_NULL, + null=True, + blank=True, + ) application_url = models.URLField( - help_text="This is a URL to the Djangonaut application form. Likely Google Forms." + help_text="This is a URL to the Djangonaut application form. Likely Google Forms.", + null=True, + blank=True, ) + objects = models.Manager.from_queryset(SessionQuerySet)() + def __str__(self): return self.title @@ -70,6 +82,11 @@ def is_accepting_applications(self): <= self.application_end_anywhere_on_earth() ) + def get_application_url(self): + if self.application_survey: + return self.application_survey.get_survey_response_url() + return self.application_url + def get_absolute_url(self): return reverse("session_detail", kwargs={"slug": self.slug}) diff --git a/home/models/survey.py b/home/models/survey.py index bdf4024b..d8f03156 100644 --- a/home/models/survey.py +++ b/home/models/survey.py @@ -1,6 +1,7 @@ from __future__ import annotations from django.db import models +from django.urls import reverse from django.utils.safestring import mark_safe from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ @@ -31,6 +32,9 @@ class Survey(BaseModel): def __str__(self): return self.name + def get_survey_response_url(self): + return reverse("survey_response_create", kwargs={"slug": self.slug}) + def save( self, force_insert=False, force_update=False, using=None, update_fields=None ): diff --git a/home/templates/home/includes/email_confirmed_warning.html b/home/templates/home/includes/email_confirmed_warning.html new file mode 100644 index 00000000..c81f142e --- /dev/null +++ b/home/templates/home/includes/email_confirmed_warning.html @@ -0,0 +1,6 @@ +{% if request.user.is_authenticated and not request.user.profile.email_confirmed %} + +{% endif %} diff --git a/home/templates/home/includes/session_apply_btn.html b/home/templates/home/includes/session_apply_btn.html new file mode 100644 index 00000000..0a6e9fca --- /dev/null +++ b/home/templates/home/includes/session_apply_btn.html @@ -0,0 +1,7 @@ +{% if session.is_accepting_applications %} + {% if session.application_url %} + Apply + {% elif request.user.is_authenticated and not session.completed_application and request.user.profile.email_confirmed %} + Apply + {% endif %} +{% endif %} diff --git a/home/templates/home/includes/session_card.html b/home/templates/home/includes/session_card.html index 8ecab07f..84c81728 100644 --- a/home/templates/home/includes/session_card.html +++ b/home/templates/home/includes/session_card.html @@ -17,9 +17,7 @@
Starts

{{ session.start_date|date:"M d, Y" }}

diff --git a/home/templates/home/prerelease/session_detail.html b/home/templates/home/prerelease/session_detail.html index 45650205..fd2268e9 100644 --- a/home/templates/home/prerelease/session_detail.html +++ b/home/templates/home/prerelease/session_detail.html @@ -14,6 +14,7 @@ {% endblock social_share %} {% block content %} +{% include 'home/includes/email_confirmed_warning.html' %}
@@ -35,12 +36,12 @@

{{ session.title }}

{{ session.end_date|date:"M d, Y" }}

{{ session.description|linebreaksbr|urlizetrunc:25 }}

- {% if session.is_accepting_applications %} + {% if session.is_accepting_applications and not session.completed_application %}

You have {{ session.application_end_anywhere_on_earth|timeuntil}} to submit your application


- Apply {% endif %} + {% include 'home/includes/session_apply_btn.html' %}
diff --git a/home/templates/home/prerelease/session_list.html b/home/templates/home/prerelease/session_list.html index 4440a0db..fba27f67 100644 --- a/home/templates/home/prerelease/session_list.html +++ b/home/templates/home/prerelease/session_list.html @@ -9,6 +9,7 @@ {% block content %} +{% include 'home/includes/email_confirmed_warning.html' %}

Sessions

diff --git a/home/tests/test_session_views.py b/home/tests/test_session_views.py index eb67afaa..ad20b561 100644 --- a/home/tests/test_session_views.py +++ b/home/tests/test_session_views.py @@ -7,7 +7,10 @@ from django.urls import reverse from freezegun import freeze_time +from accounts.factories import UserFactory from home.factories import SessionFactory +from home.factories import SurveyFactory +from home.factories import UserSurveyResponseFactory @freeze_time("2023-11-16") @@ -23,6 +26,16 @@ def setUpTestData(cls): application_start_date=datetime(2023, 10, 16).date(), application_end_date=datetime(2023, 11, 15).date(), ) + cls.survey = SurveyFactory.create(name="Application Survey") + cls.session_application_open_with_survey = SessionFactory.create( + application_start_date=datetime(2023, 10, 16).date(), + application_end_date=datetime(2023, 11, 15).date(), + application_url=None, + application_survey=cls.survey, + ) + cls.survey_url = reverse( + "survey_response_create", kwargs={"slug": cls.survey.slug} + ) cls.session_application_closed = SessionFactory.create( invitation_date=datetime(2023, 6, 30).date(), application_start_date=datetime(2023, 6, 1).date(), @@ -34,9 +47,55 @@ def test_session_list(self): self.assertEqual(response.status_code, 200) self.assertTemplateUsed("home/prerelease/session_list.html") self.assertContains(response, self.session_application_open.application_url) + self.assertNotContains(response, self.survey_url) + self.assertNotContains( + response, self.session_application_closed.application_url + ) + self.assertNotContains(response, "Your email is not confirmed!") + self.assertNotContains(response, "You may not be able to apply for sessions") + + def test_session_list_email_not_confirmed(self): + user = UserFactory.create(profile__email_confirmed=False) + self.client.force_login(user) + response = self.client.get(reverse("session_list")) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed("home/prerelease/session_list.html") + self.assertContains(response, self.session_application_open.application_url) + self.assertNotContains(response, self.survey_url) + self.assertNotContains( + response, self.session_application_closed.application_url + ) + self.assertContains(response, "Your email is not confirmed!") + self.assertContains(response, "You may not be able to apply for sessions") + + def test_session_list_email_confirmed(self): + user = UserFactory.create(profile__email_confirmed=True) + self.client.force_login(user) + response = self.client.get(reverse("session_list")) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed("home/prerelease/session_list.html") + self.assertContains(response, self.session_application_open.application_url) + self.assertContains(response, self.survey_url) + self.assertNotContains( + response, self.session_application_closed.application_url + ) + self.assertNotContains(response, "Your email is not confirmed!") + self.assertNotContains(response, "You may not be able to apply for sessions") + + def test_session_list_email_confirmed_already_applied(self): + user = UserFactory.create(profile__email_confirmed=True) + UserSurveyResponseFactory(survey=self.survey, user=user) + self.client.force_login(user) + response = self.client.get(reverse("session_list")) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed("home/prerelease/session_list.html") + self.assertContains(response, self.session_application_open.application_url) + self.assertNotContains(response, self.survey_url) self.assertNotContains( response, self.session_application_closed.application_url ) + self.assertNotContains(response, "Your email is not confirmed!") + self.assertNotContains(response, "You may not be able to apply for sessions") def test_session_detail_open_application(self): url = reverse( @@ -52,6 +111,71 @@ def test_session_detail_open_application(self): response.rendered_content.split() ), # Remove the non-breaking spaces ) + self.assertNotContains(response, "Your email is not confirmed!") + self.assertNotContains(response, "You may not be able to apply for sessions") + + def test_session_detail_open_application_with_survey_email_not_confirmed(self): + user = UserFactory.create(profile__email_confirmed=False) + self.client.force_login(user) + url = reverse( + "session_detail", + kwargs={"slug": self.session_application_open_with_survey.slug}, + ) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed("home/prerelease/session_detail.html") + self.assertNotContains(response, self.survey_url) + self.assertIn( + "You have 11 hours, 59 minutes to submit your application", + " ".join( + response.rendered_content.split() + ), # Remove the non-breaking spaces + ) + self.assertContains(response, "Your email is not confirmed!") + self.assertContains(response, "You may not be able to apply for sessions") + + def test_session_detail_open_application_with_survey_email_confirmed(self): + user = UserFactory.create(profile__email_confirmed=True) + self.client.force_login(user) + url = reverse( + "session_detail", + kwargs={"slug": self.session_application_open_with_survey.slug}, + ) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed("home/prerelease/session_detail.html") + self.assertContains(response, self.survey_url) + self.assertIn( + "You have 11 hours, 59 minutes to submit your application", + " ".join( + response.rendered_content.split() + ), # Remove the non-breaking spaces + ) + self.assertNotContains(response, "Your email is not confirmed!") + self.assertNotContains(response, "You may not be able to apply for sessions") + + def test_session_detail_open_application_with_survey_email_confirmed_already_applied( + self, + ): + user = UserFactory.create(profile__email_confirmed=True) + UserSurveyResponseFactory(survey=self.survey, user=user) + self.client.force_login(user) + url = reverse( + "session_detail", + kwargs={"slug": self.session_application_open_with_survey.slug}, + ) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed("home/prerelease/session_detail.html") + self.assertNotContains(response, self.survey_url) + self.assertNotIn( + "You have 11 hours, 59 minutes to submit your application", + " ".join( + response.rendered_content.split() + ), # Remove the non-breaking spaces + ) + self.assertNotContains(response, "Your email is not confirmed!") + self.assertNotContains(response, "You may not be able to apply for sessions") def test_session_detail_closed_application(self): url = reverse( diff --git a/home/tests/test_user_survey_response_form_views.py b/home/tests/test_user_survey_response_form_views.py index 680ab54e..4cbc0588 100644 --- a/home/tests/test_user_survey_response_form_views.py +++ b/home/tests/test_user_survey_response_form_views.py @@ -18,7 +18,7 @@ def setUpTestData(cls): name="Test Survey", description="This is a description of the survey!" ) cls.url = reverse("survey_response_create", kwargs={"slug": cls.survey.slug}) - cls.user = UserFactory.create() + cls.user = UserFactory.create(profile__email_confirmed=True) cls.question = QuestionFactory.create( survey=cls.survey, label="How are you?", @@ -28,12 +28,18 @@ def test_login_required(self): response = self.client.get(self.url, follow=True) self.assertRedirects(response, f"{reverse('login')}?next={self.url}") + def test_email_confirmed_required(self): + self.user.profile.email_confirmed = False + self.user.profile.save() + self.client.force_login(self.user) + response = self.client.get(self.url) + self.assertEqual(response.status_code, 403) + def test_only_one_per_user(self): self.client.force_login(self.user) UserSurveyResponseFactory(survey=self.survey, user=self.user) - response = self.client.get(self.url, follow=True) - self.assertContains(response, "You have already submitted.") - self.assertRedirects(response, reverse("session_list")) + response = self.client.get(self.url) + self.assertEqual(response.status_code, 403) def test_success_get(self): self.client.force_login(self.user) diff --git a/home/views.py b/home/views.py index 7831b432..bec9f310 100644 --- a/home/views.py +++ b/home/views.py @@ -3,11 +3,10 @@ from gettext import gettext from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.shortcuts import redirect +from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.auth.mixins import UserPassesTestMixin from django.shortcuts import render from django.urls import reverse_lazy -from django.utils.decorators import method_decorator from django.views.generic.detail import DetailView from django.views.generic.edit import FormMixin from django.views.generic.list import ListView @@ -92,27 +91,35 @@ class SessionDetailView(DetailView): model = Session template_name = "home/prerelease/session_detail.html" + def get_queryset(self): + return Session.objects.with_applications(user=self.request.user) + class SessionListView(ListView): model = Session template_name = "home/prerelease/session_list.html" context_object_name = "sessions" + def get_queryset(self): + return Session.objects.with_applications(user=self.request.user) + -@method_decorator(login_required, name="dispatch") -class CreateUserSurveyResponseFormView(FormMixin, DetailView): +class CreateUserSurveyResponseFormView( + LoginRequiredMixin, UserPassesTestMixin, FormMixin, DetailView +): model = Survey object = None form_class = CreateUserSurveyResponseForm success_url = reverse_lazy("session_list") template_name = "home/surveys/form.html" - def dispatch(self, request, *args, **kwargs): + def test_func(self): survey = self.get_object() - if UserSurveyResponse.objects.filter(survey=survey, user=request.user).exists(): - messages.warning(request, gettext("You have already submitted.")) - return redirect("session_list") - return super().dispatch(request, *args, **kwargs) + user = self.request.user + return ( + user.profile.email_confirmed + and not UserSurveyResponse.objects.filter(survey=survey, user=user).exists() + ) def get_form_kwargs(self): kwargs = super().get_form_kwargs()