Skip to content

Commit

Permalink
Add permission checking for session applications and warnings when em…
Browse files Browse the repository at this point in the history
…ail is not confirmed. (#340)



Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Tim Schilling <[email protected]>
  • Loading branch information
3 people authored Mar 29, 2024
1 parent 1bc5226 commit 095de2e
Show file tree
Hide file tree
Showing 13 changed files with 245 additions and 21 deletions.
2 changes: 1 addition & 1 deletion home/management/commands/applications_open_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
Expand Down
18 changes: 18 additions & 0 deletions home/managers.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand Down
35 changes: 35 additions & 0 deletions home/migrations/0026_session_application_survey_and_more.py
Original file line number Diff line number Diff line change
@@ -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,
),
),
]
19 changes: 18 additions & 1 deletion home/models/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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})

Expand Down
4 changes: 4 additions & 0 deletions home/models/survey.py
Original file line number Diff line number Diff line change
@@ -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 _
Expand Down Expand Up @@ -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
):
Expand Down
6 changes: 6 additions & 0 deletions home/templates/home/includes/email_confirmed_warning.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{% if request.user.is_authenticated and not request.user.profile.email_confirmed %}
<div class="my-2 mx-2 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded" role="alert">
<strong class="font-bold">Your email is not confirmed!</strong>
<span class="block sm:inline">You may not be able to apply for sessions without <a href="{% url 'profile' %}" class="underline">confirming your email address.</a></span>
</div>
{% endif %}
7 changes: 7 additions & 0 deletions home/templates/home/includes/session_apply_btn.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% if session.is_accepting_applications %}
{% if session.application_url %}
<a href="{{ session.application_url }}" target="_blank" class="rounded-md bg-primary px-5 py-2.5 text-sm font-medium text-white transition hover:bg-gray-300 hover:text-ds-purple cursor-pointer">Apply</a>
{% elif request.user.is_authenticated and not session.completed_application and request.user.profile.email_confirmed %}
<a href="{{ session.get_application_url }}" target="_blank" class="rounded-md bg-primary px-5 py-2.5 text-sm font-medium text-white transition hover:bg-gray-300 hover:text-ds-purple cursor-pointer">Apply</a>
{% endif %}
{% endif %}
4 changes: 1 addition & 3 deletions home/templates/home/includes/session_card.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ <h6 class="text-lg">Starts</h6>
<p>{{ session.start_date|date:"M d, Y" }}</p>
</div>
<div class="px-6 py-3 mb-2 card-footer">
{% if session.is_accepting_applications %}
<a href="{{ session.application_url }}" target="_blank" class="bg-purple-500 hover:bg-purple-400 no-underline text-white font-bold py-2 px-4 rounded">Apply</a>
{% endif %}
{% include 'home/includes/session_apply_btn.html' %}
<a href="{{ session.get_absolute_url }}" class="card-link">Details</a>
</div>
</div>
Expand Down
5 changes: 3 additions & 2 deletions home/templates/home/prerelease/session_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
{% endblock social_share %}

{% block content %}
{% include 'home/includes/email_confirmed_warning.html' %}
<div class="container main my-3 mx-3">
<div class="row my-3">
<div class="col">
Expand All @@ -35,12 +36,12 @@ <h1 class="text-5xl my-3">{{ session.title }}</h1>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{{ session.end_date|date:"M d, Y" }}</dd>
</dl>
<p>{{ session.description|linebreaksbr|urlizetrunc:25 }}</p>
{% if session.is_accepting_applications %}
{% if session.is_accepting_applications and not session.completed_application %}
<br />
<h2 class="text-4xl">You have {{ session.application_end_anywhere_on_earth|timeuntil}} to submit your application</h2>
<br />
<a href="{{ session.application_url }}" target="_blank" class="bg-purple-500 hover:bg-purple-400 no-underline text-white font-bold py-2 px-4 rounded">Apply</a>
{% endif %}
{% include 'home/includes/session_apply_btn.html' %}
</div>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions home/templates/home/prerelease/session_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@


{% block content %}
{% include 'home/includes/email_confirmed_warning.html' %}
<main class="section my-3 mx-5">
<div class="section-container">
<h1 class="text-5xl">Sessions</h1>
Expand Down
124 changes: 124 additions & 0 deletions home/tests/test_session_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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(),
Expand All @@ -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(
Expand All @@ -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(
Expand Down
14 changes: 10 additions & 4 deletions home/tests/test_user_survey_response_form_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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?",
Expand All @@ -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)
Expand Down
Loading

0 comments on commit 095de2e

Please sign in to comment.