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 %}
+
+
Your email is not confirmed!
+
You may not be able to apply for sessions without confirming your email address.
+
+{% 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()