diff --git a/ws/models.py b/ws/models.py
index 2b7c2e9d..7a3b4b75 100644
--- a/ws/models.py
+++ b/ws/models.py
@@ -817,7 +817,7 @@ class BaseRating(models.Model):
notes = models.TextField(max_length=500, blank=True) # Contingencies, etc.
@property
- def activity_enum(self):
+ def activity_enum(self) -> enums.Activity:
return enums.Activity(self.activity)
def __str__(self):
@@ -1674,7 +1674,7 @@ def can_reapply(cls, latest_application):
return time_passed > timedelta(days=waiting_period_days)
@property
- def activity(self):
+ def activity(self) -> str:
"""Extract the activity name from the class name/db_name.
Meant to be used by inheriting classes, for example:
diff --git a/ws/templates/leaders/apply_any_activity.html b/ws/templates/leaders/apply_any_activity.html
new file mode 100644
index 00000000..10dc1be8
--- /dev/null
+++ b/ws/templates/leaders/apply_any_activity.html
@@ -0,0 +1,28 @@
+{% extends "base.html" %}
+{% load application_tags %}
+{% load crispy_forms_tags %}
+
+{% block head_title %}Apply to be a leader{% endblock head_title %}
+
+{% block content %}
+{{ block.super }}
+
+
Become a MITOC leader
+
+
+ MITOC's volunteer leaders run trips for the community.
+
+
+
+ MITOCers may apply to become a leader, at the discretion of the activity chair(s).
+
+
+Activities which accept leader applications:
+
+
+
+{% endblock content %}
diff --git a/ws/tests/views/test_applications.py b/ws/tests/views/test_applications.py
index ec6b43b3..858d6fb6 100644
--- a/ws/tests/views/test_applications.py
+++ b/ws/tests/views/test_applications.py
@@ -1,4 +1,7 @@
+from unittest import mock
+
from bs4 import BeautifulSoup
+from django.contrib import messages
from django.contrib.auth.models import Group
from django.test import Client, TestCase
from freezegun import freeze_time
@@ -127,7 +130,63 @@ class BikingLeaderApplicationTest(TestCase):
def test_404(self):
self.client.force_login(factories.ParticipantFactory.create().user)
response = self.client.get('/biking/leaders/apply/')
- self.assertEqual(response.status_code, 404)
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response.url, "/leaders/apply/")
+
+
+class AnyActivityLeaderApplyViewTest(TestCase):
+ def test_anonymous_user_can_see_page(self):
+ response = self.client.get('/leaders/apply/')
+ self.assertEqual(response.status_code, 200)
+ self.assertIn(
+ b"MITOC's volunteer leaders run trips for the community",
+ response.content,
+ )
+ # 3 activities (at time of writing) which accept applications
+ self.assertIn(b"Climbing", response.content)
+ self.assertIn(b"/climbing/leaders/apply/", response.content)
+ self.assertIn(b"Hiking", response.content)
+ self.assertIn(b"/hiking/leaders/apply/", response.content)
+ self.assertIn(b"Winter School", response.content)
+ self.assertIn(b"/winter_school/leaders/apply/", response.content)
+
+ def test_redirected_from_unknown_activity(self):
+ self.client.force_login(factories.ParticipantFactory.create().user)
+ with mock.patch.object(messages, 'error', wraps=messages.error) as msg_error:
+ response = self.client.get('/raccoon_wrestling/leaders/apply/')
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response.url, "/leaders/apply/")
+ msg_error.assert_called_once_with(
+ mock.ANY, 'raccoon_wrestling is not a known activity.'
+ )
+
+ def test_redirected_from_activity_without_application(self):
+ self.client.force_login(factories.ParticipantFactory.create().user)
+ with mock.patch.object(messages, 'error', wraps=messages.error) as msg_error:
+ response = self.client.get('/cabin/leaders/apply/')
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response.url, "/leaders/apply/")
+ msg_error.assert_called_once_with(
+ mock.ANY, 'Cabin is not accepting leader applications'
+ )
+
+ def test_no_underscores(self):
+ """Reproduces a somewhat odd case where stripping underscores led to a 500.
+
+ This was because our usual "activity is valid" checks first stripped underscores,
+ but then `winterschool` was expected to be a valid enum value & was not.
+ """
+ self.client.force_login(factories.ParticipantFactory.create().user)
+ with mock.patch.object(messages, 'error', wraps=messages.error) as msg_error:
+ response = self.client.get('/winterschool/leaders/apply/')
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response.url, "/leaders/apply/")
+ msg_error.assert_called_once_with(
+ mock.ANY, 'winterschool is not a known activity.'
+ )
+ self.assertIn(
+ b"/winter_school/leaders/apply/", self.client.get(response.url).content
+ )
class HikingLeaderApplicationTest(TestCase, Helpers):
diff --git a/ws/urls.py b/ws/urls.py
index 6489aa8f..8c2629c0 100644
--- a/ws/urls.py
+++ b/ws/urls.py
@@ -169,8 +169,8 @@
path('profile/edit/', views.EditProfileView.as_view(), name='edit_profile'),
path(
'leaders/apply/',
- RedirectView.as_view(url='/winter_school/leaders/apply', permanent=True),
- name='old_become_leader',
+ views.AnyActivityLeaderApplyView.as_view(),
+ name='leaders_apply',
),
path('profile/membership/', views.PayDuesView.as_view(), name='pay_dues'),
path('profile/waiver/', views.SignWaiverView.as_view(), name='initiate_waiver'),
diff --git a/ws/views/applications.py b/ws/views/applications.py
index 1ad7c09b..d5219f29 100644
--- a/ws/views/applications.py
+++ b/ws/views/applications.py
@@ -16,10 +16,10 @@
from django.db.models.functions import Cast, Least
from django.forms.models import model_to_dict
from django.http import Http404
-from django.shortcuts import render
+from django.shortcuts import redirect, render
from django.urls import reverse, reverse_lazy
from django.utils.decorators import method_decorator
-from django.views.generic import CreateView, DetailView, ListView
+from django.views.generic import CreateView, DetailView, ListView, TemplateView
from django.views.generic.edit import FormMixin
import ws.utils.perms as perm_utils
@@ -144,11 +144,35 @@ def get_context_data(self, **kwargs):
@method_decorator(user_info_required)
def dispatch(self, request, *args, **kwargs):
activity = kwargs.get('activity')
- if not models.LeaderApplication.can_apply_for_activity(activity):
- raise Http404
+ try:
+ activity_enum = enums.Activity(activity)
+ except ValueError: # (Not a valid activity)
+ messages.error(self.request, f"{activity} is not a known activity.")
+ return redirect(reverse('leaders_apply'))
+
+ if not models.LeaderApplication.can_apply_for_activity(activity_enum):
+ messages.error(
+ self.request,
+ f"{activity_enum.label} is not accepting leader applications",
+ )
+ return redirect(reverse('leaders_apply'))
+
return super().dispatch(request, *args, **kwargs)
+class AnyActivityLeaderApplyView(TemplateView):
+ template_name = 'leaders/apply_any_activity.html'
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context['activities_accepting_applications'] = [
+ activity_enum
+ for activity_enum in enums.Activity
+ if models.LeaderApplication.can_apply_for_activity(activity_enum)
+ ]
+ return context
+
+
# model is a property on LeaderApplicationMixin, but a class attribute on MultipleObjectMixin
class AllLeaderApplicationsView(ApplicationManager, ListView): # type: ignore[misc]
context_object_name = 'leader_applications'