From 8de2a59db9e38a78c5b5ab85ff7a8587aa5cd6e3 Mon Sep 17 00:00:00 2001
From: Eric <34304046+EricTRL@users.noreply.github.com>
Date: Sat, 7 Sep 2024 20:11:43 +0200
Subject: [PATCH] Room access requirements + card updates
Remove external_card_cluster (not used anymore)
All card numbers require 8 digits now (all 7-length numbers are prepended with a 0)
External card digits require 4 numbers (all existing 3-digit numbers are prepended with a 0)
Rooms now have an access type (card, key, other)
When registering a member, Squire no longer requires a phone number if 1) only rooms with card access are requested and 2) a tue card number is provided
Updated Rickrolls to a themed variant
---
core/templates/martor/bootstrap/guide.html | 2 +-
membership_file/admin.py | 26 ++++++++---
.../fixtures/test_export_members.json | 8 ++--
membership_file/fixtures/test_members.json | 4 +-
membership_file/forms.py | 20 +++++++--
membership_file/migrations/0019_room_cards.py | 42 ++++++++++++++++++
.../migrations/0020_card_lengths.py | 32 ++++++++++++++
.../migrations/0021_card_lengths_db.py | 33 ++++++++++++++
membership_file/models.py | 43 ++++++++++++-------
.../membership_file/membership_edit.html | 2 +-
.../membership_file/membership_view.html | 2 +-
membership_file/tests/test_forms.py | 25 +++++++++--
membership_file/tests/test_views.py | 2 +-
membership_file/tests/tests_admin.py | 1 -
membership_file/tests/tests_model.py | 7 ++-
membership_file/tests/tests_user.py | 13 +-----
user_interaction/static/themes/april-theme.js | 4 +-
user_interaction/views.py | 8 ++++
18 files changed, 219 insertions(+), 55 deletions(-)
create mode 100644 membership_file/migrations/0019_room_cards.py
create mode 100644 membership_file/migrations/0020_card_lengths.py
create mode 100644 membership_file/migrations/0021_card_lengths_db.py
diff --git a/core/templates/martor/bootstrap/guide.html b/core/templates/martor/bootstrap/guide.html
index 2196a3ae..39c23bae 100644
--- a/core/templates/martor/bootstrap/guide.html
+++ b/core/templates/martor/bootstrap/guide.html
@@ -109,7 +109,7 @@
Ctrl+L |
Command+L |
- Link |
+ Link |
diff --git a/membership_file/admin.py b/membership_file/admin.py
index aa5c23c6..be3af623 100644
--- a/membership_file/admin.py
+++ b/membership_file/admin.py
@@ -164,7 +164,7 @@ def get_export_filename(self, request, queryset, file_format):
('street', 'house_number', 'house_number_addition'), ('postal_code', 'city'), 'country']}),
('Room Access', {'fields':
['key_id', 'tue_card_number',
- ('external_card_number', 'external_card_digits', 'external_card_cluster'),
+ ('external_card_number', 'external_card_digits'),
'external_card_deposit', 'accessible_rooms']}),
('Legal Information', {'fields':
['educational_institution', 'student_number',
@@ -266,13 +266,29 @@ class MemberLogReadOnly(DisableModificationsAdminMixin, HideRelatedNameAdmin):
class RoomAdmin(admin.ModelAdmin):
model = Room
- list_display = ("id", "name", "access")
+ list_display = ("id", "name", "room_number", "access_type", "access_specification")
list_display_links = ("id", "name")
- search_fields = ["name", "access"]
-
- ordering = ("access",)
+ search_fields = ["name", "room_number"]
+ ordering = ("access_type", "access_specification")
filter_horizontal = ("members_with_access",)
+ fieldsets = [
+ (
+ None,
+ {
+ "fields": [
+ (
+ "name",
+ "room_number",
+ ),
+ ("access_type", "access_specification"),
+ "notes",
+ "members_with_access",
+ ]
+ },
+ ),
+ ]
+
@admin.register(MemberYear)
class MemberYearAdmin(ExportActionMixin, admin.ModelAdmin):
diff --git a/membership_file/fixtures/test_export_members.json b/membership_file/fixtures/test_export_members.json
index bcc91208..d63a7274 100644
--- a/membership_file/fixtures/test_export_members.json
+++ b/membership_file/fixtures/test_export_members.json
@@ -10,9 +10,8 @@
"last_name": "Doe",
"student_number": "1234567",
"educational_institution": "TUe",
- "external_card_number": "1234567",
+ "external_card_number": "01234567",
"external_card_digits": "",
- "external_card_cluster": "Luna Cluster 3",
"email": "johndoe@example.com",
"last_updated_date": "1970-01-01T12:00:00Z",
"last_updated_by": null
@@ -38,7 +37,8 @@
"pk": 1,
"fields": {
"name": "Room 1",
- "access": "Key 12",
+ "access_type": "KEY",
+ "access_specification": "12",
"members_with_access": [
1
]
@@ -49,7 +49,7 @@
"pk": 2,
"fields": {
"name": "Room 2",
- "access": "Campus Card",
+ "access_type": "CARD",
"members_with_access": [
1
]
diff --git a/membership_file/fixtures/test_members.json b/membership_file/fixtures/test_members.json
index a7a0ab45..b071174d 100644
--- a/membership_file/fixtures/test_members.json
+++ b/membership_file/fixtures/test_members.json
@@ -10,10 +10,9 @@
"last_name": "Dommel",
"student_number": "0987654",
"educational_institution": "TUe",
- "tue_card_number": "1234567",
+ "tue_card_number": "01234567",
"external_card_number": null,
"external_card_digits": "",
- "external_card_cluster": "",
"email": "linked_member@example.com",
"phone_number": null,
"street": "John F. Kennedylaan",
@@ -44,7 +43,6 @@
"tue_card_number": null,
"external_card_number": null,
"external_card_digits": "",
- "external_card_cluster": "",
"email": "nonlinked_member@example.com",
"phone_number": null,
"street": "Limbopad",
diff --git a/membership_file/forms.py b/membership_file/forms.py
index 53e0ac61..44718b73 100644
--- a/membership_file/forms.py
+++ b/membership_file/forms.py
@@ -327,10 +327,24 @@ def clean(self) -> Dict[str, Any]:
res = super().clean()
# Phone number requirements
if not self.cleaned_data["phone_number"] and self.cleaned_data["room_access"]:
- self.add_error(
- "phone_number",
- ValidationError("A phone number is required if room access is provided.", code="phone_required"),
+ any_room_not_card = any(
+ [
+ Room.objects.get(id=room_id).access_type != Room.ACCESS_CARD
+ for room_id in self.cleaned_data["room_access"]
+ ]
)
+ # There is >=1 room that uses a key, or all rooms require card access and no TUe card number was provided
+ if any_room_not_card or not self.cleaned_data["tue_card_number"]:
+ msg = "A phone number is required; access was requested for rooms with keys."
+ if not any_room_not_card:
+ msg = "A phone number is required, or a TUe card number should be entered. Access was only requested for rooms with card access."
+ self.add_error(
+ "phone_number",
+ ValidationError(
+ msg,
+ code="phone_required",
+ ),
+ )
# Educational institution requirements
if (
diff --git a/membership_file/migrations/0019_room_cards.py b/membership_file/migrations/0019_room_cards.py
new file mode 100644
index 00000000..d151e12a
--- /dev/null
+++ b/membership_file/migrations/0019_room_cards.py
@@ -0,0 +1,42 @@
+# Generated by Django 3.2.25 on 2024-09-07 11:50
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("membership_file", "0018_membership_member_null"),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name="room",
+ options={},
+ ),
+ migrations.RenameField(
+ model_name="room",
+ old_name="access",
+ new_name="access_specification",
+ ),
+ migrations.AddField(
+ model_name="room",
+ name="access_type",
+ field=models.CharField(
+ choices=[("KEY", "Key"), ("CARD", "TUe Card/External Card"), ("MISC", "Other")],
+ default="MISC",
+ max_length=4,
+ ),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name="room",
+ name="room_number",
+ field=models.CharField(blank=True, max_length=15),
+ ),
+ migrations.AlterField(
+ model_name="room",
+ name="access_specification",
+ field=models.CharField(blank=True, help_text="E.g. key number", max_length=15),
+ ),
+ ]
diff --git a/membership_file/migrations/0020_card_lengths.py b/membership_file/migrations/0020_card_lengths.py
new file mode 100644
index 00000000..ac311424
--- /dev/null
+++ b/membership_file/migrations/0020_card_lengths.py
@@ -0,0 +1,32 @@
+# Generated by Django 3.2.25 on 2024-09-07 16:47
+
+from django.db import migrations
+
+
+def extend_card_numbers(apps, schema_editor):
+ """Extends the tue/external card numbers to 8 digits, and the external card digits to 4 digits"""
+ # We can't import the Person model directly as it may be a newer
+ # version than this migration expects. We use the historical version.
+ Member = apps.get_model("membership_file", "Member")
+ for member in Member.objects.all():
+ if member.external_card_digits:
+ member.external_card_digits = member.external_card_digits.zfill(4)
+ member.last_updated_by = None
+ if member.external_card_number:
+ member.external_card_number = member.external_card_number.zfill(8)
+ member.last_updated_by = None
+ if member.tue_card_number:
+ member.tue_card_number = member.tue_card_number.zfill(8)
+ member.last_updated_by = None
+ member.save()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("membership_file", "0019_room_cards"),
+ ]
+
+ operations = [
+ migrations.RunPython(extend_card_numbers),
+ ]
diff --git a/membership_file/migrations/0021_card_lengths_db.py b/membership_file/migrations/0021_card_lengths_db.py
new file mode 100644
index 00000000..14bd2de8
--- /dev/null
+++ b/membership_file/migrations/0021_card_lengths_db.py
@@ -0,0 +1,33 @@
+# Generated by Django 3.2.25 on 2024-09-07 16:48
+
+import django.core.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('membership_file', '0020_card_lengths'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='member',
+ name='external_card_cluster',
+ ),
+ migrations.AlterField(
+ model_name='member',
+ name='external_card_digits',
+ field=models.CharField(blank=True, max_length=4, validators=[django.core.validators.RegexValidator(message='External card digits must consist of exactly 4 digits. E.g. 0012', regex='^[0-9]{4}$')], verbose_name='digits'),
+ ),
+ migrations.AlterField(
+ model_name='member',
+ name='external_card_number',
+ field=models.CharField(blank=True, help_text="External cards mention 'FMC', whereas Tu/e cards are currently red (since sept. 2021) or orange (before sept. 2021).", max_length=15, null=True, validators=[django.core.validators.RegexValidator(message='TUe card numbers must only consist of exactly 8 digits. For older cards (pre 2017) that only used 7 digits, prepend a 0. E.g. 01457347', regex='^[0-9]{8}$')]),
+ ),
+ migrations.AlterField(
+ model_name='member',
+ name='tue_card_number',
+ field=models.CharField(blank=True, max_length=15, null=True, unique=True, validators=[django.core.validators.RegexValidator(message='TUe card numbers must only consist of exactly 8 digits. For older cards (pre 2017) that only used 7 digits, prepend a 0. E.g. 01457347', regex='^[0-9]{8}$')], verbose_name='TUe card number'),
+ ),
+ ]
diff --git a/membership_file/models.py b/membership_file/models.py
index 70821e3d..339266ac 100644
--- a/membership_file/models.py
+++ b/membership_file/models.py
@@ -82,7 +82,8 @@ class Meta:
# NB: These numbers may start with 0, which is why they are not IntegerFields
##################################
tue_card_number_regex = RegexValidator(
- regex=r"^[0-9]{7,8}$", message="TUe card numbers must only consist of exactly 7 or 8 digits. E.g. 1234567"
+ regex=r"^[0-9]{8}$",
+ message="TUe card numbers must only consist of exactly 8 digits. For older cards (pre 2017) that only used 7 digits, prepend a 0. E.g. 01457347",
)
tue_card_number = models.CharField(
validators=[tue_card_number_regex],
@@ -94,7 +95,7 @@ class Meta:
)
external_card_digits_regex = RegexValidator(
- regex=r"^[0-9]{3}$", message="External card digits must consist of exactly 3 digits. E.g. 012"
+ regex=r"^[0-9]{4}$", message="External card digits must consist of exactly 4 digits. E.g. 0012"
)
# External card uses the same number formatting as Tue cards, but its number does not necessarily need to be unique
@@ -103,14 +104,12 @@ class Meta:
max_length=15,
null=True,
blank=True,
- help_text="External cards are blue, whereas Tu/e cards are currently red (since sept. 2021) or orange (before sept. 2021).",
+ help_text="External cards mention 'FMC', whereas Tu/e cards are currently red (since sept. 2021) or orange (before sept. 2021).",
)
- # 3-digit code at the bottom of a card
+ # 4-digit code at the bottom of a card; only used for external cards
external_card_digits = models.CharField(
- validators=[external_card_digits_regex], max_length=3, blank=True, verbose_name="digits"
+ validators=[external_card_digits_regex], max_length=4, blank=True, verbose_name="digits"
)
- # The cluster contains additional information of an external card
- external_card_cluster = models.CharField(max_length=255, blank=True, verbose_name="cluster")
# External cards require a deposit, which has changed over the years
external_card_deposit = models.DecimalField(
@@ -257,13 +256,9 @@ def display_external_card_number(self):
display_card = self.external_card_number
if self.external_card_digits:
- # Not all external card have a 3-digit code (E.g. parking cards)
+ # Not all external card have a 4-digit code (E.g. parking cards)
display_card += f"-{self.external_card_digits}"
- if self.external_card_cluster:
- # Not all external cards have a cluster
- display_card += f" ({self.external_card_cluster})"
-
return display_card
# Displays a user's address
@@ -286,11 +281,22 @@ def display_address(self):
class Room(models.Model):
- class Meta:
- ordering = ["access", "name"]
+ """A room that can be gained access through by a card or key."""
name = models.CharField(max_length=63)
- access = models.CharField(max_length=15, help_text="How access is provided. E.g. 'Key 12' or 'Campus Card'")
+ room_number = models.CharField(max_length=15, blank=True)
+
+ ACCESS_KEY = "KEY"
+ ACCESS_CARD = "CARD"
+ ACCESS_OTHER = "MISC"
+ _ACCESS_TYPES = [
+ (ACCESS_KEY, "Key"),
+ (ACCESS_CARD, "TUe Card/External Card"),
+ (ACCESS_OTHER, "Other"),
+ ]
+ access_type = models.CharField(max_length=4, choices=_ACCESS_TYPES)
+ access_specification = models.CharField(max_length=15, help_text="E.g. key number", blank=True)
+
notes = models.TextField(blank=True)
# Members who have access to this room
@@ -299,7 +305,12 @@ class Meta:
members_with_access = models.ManyToManyField(Member, blank=True, related_name="accessible_rooms")
def __str__(self):
- return f"{self.name} ({self.access})"
+ access = self.get_access_type_display()
+ if self.access_specification:
+ access += " - " + self.access_specification
+ if self.room_number:
+ return f"{self.room_number} - {self.name} ({access})"
+ return f"{self.name} ({access})"
##################################################################################
diff --git a/membership_file/templates/membership_file/membership_edit.html b/membership_file/templates/membership_file/membership_edit.html
index 0a452da5..4f8af6dd 100644
--- a/membership_file/templates/membership_file/membership_edit.html
+++ b/membership_file/templates/membership_file/membership_edit.html
@@ -43,7 +43,7 @@ Contact Details
Room Access
{% generic_field form.key_id -1 %}
{% generic_field form.tue_card_number -1 %}
- {% generic_field form.external_card_number form.external_card_digits form.external_card_cluster 110 70 -1 %}
+ {% generic_field form.external_card_number form.external_card_digits 110 70 %}
{% if form.external_card_number.value %}
{% generic_field form.external_card_deposit -1 %}
diff --git a/membership_file/templates/membership_file/membership_view.html b/membership_file/templates/membership_file/membership_view.html
index 1d75ac76..bae25359 100644
--- a/membership_file/templates/membership_file/membership_view.html
+++ b/membership_file/templates/membership_file/membership_view.html
@@ -138,7 +138,7 @@ Membership Card
{% if member.last_updated_by is not None %}
This information was last changed on {{member.last_updated_date|date:"M j, Y \a\t H\:i"}} by {{member.display_last_updated_name}}
{% else %}
- This information was never updated since its creation on {{member.last_updated_date|date:"M j, Y \a\t H\:i"}}
+ This information was last changed on {{member.last_updated_date|date:"M j, Y \a\t H\:i"}}
{% endif %}
{% if member.marked_for_deletion %}
diff --git a/membership_file/tests/test_forms.py b/membership_file/tests/test_forms.py
index 98dc100f..2f44941a 100644
--- a/membership_file/tests/test_forms.py
+++ b/membership_file/tests/test_forms.py
@@ -110,7 +110,7 @@ def test_fields(self, _):
)
# Rooms become available once they exist
- kitchen = Room.objects.create(name="Kitchen", access="Crowbar")
+ kitchen = Room.objects.create(name="Kitchen", access_type=Room.ACCESS_OTHER, access_specification="Crowbar")
self.assertHasField("room_access", required=False, initial=None, choices=[(kitchen.id, str(kitchen))])
# Fieldsets should exist
@@ -120,9 +120,26 @@ def test_fields(self, _):
def test_clean(self, _):
"""Tests field cleaning"""
- # Phone number should be provided if room access is requested
+ # Phone number should be provided if room access is requested for rooms with keys,
+ # even if a tue card number is provided
self.assertFormHasError(
- {"room_access": [Room.objects.create(name="Kitchen", access="Crowbar").pk]},
+ {
+ "room_access": [
+ Room.objects.create(name="Kitchen", access_type=Room.ACCESS_OTHER).pk,
+ Room.objects.create(name="Basement", access_type=Room.ACCESS_CARD).pk,
+ ],
+ "tue_card_number": "01234567",
+ },
+ "phone_required",
+ field_name="phone_number",
+ )
+
+ # If all requested rooms requires card access, and a tue card is provided, then no phone number is needed
+ self.assertFormNotHasError(
+ {
+ "room_access": [Room.objects.create(name="Basement", access_type=Room.ACCESS_CARD).pk],
+ "tue_card_number": "01234567",
+ },
"phone_required",
field_name="phone_number",
)
@@ -173,7 +190,7 @@ def test_save(self, _):
self.assertFormValid(data)
# Room Access
- room = Room.objects.create(name="Kitchen", access="Crowbar")
+ room = Room.objects.create(name="Kitchen", access_type=Room.ACCESS_OTHER, access_specification="Crowbar")
data = {**data, "room_access": [room.pk]}
form = self.build_form(data)
self.assertFormValid(data)
diff --git a/membership_file/tests/test_views.py b/membership_file/tests/test_views.py
index ce783753..5e5c5dfa 100644
--- a/membership_file/tests/test_views.py
+++ b/membership_file/tests/test_views.py
@@ -150,7 +150,7 @@ def test_messages(self):
def test_admin_log(self):
"""Tests if an admin log entry is created"""
- Room.objects.create(name="Room", access="Master Key")
+ Room.objects.create(name="Room", access_type=Room.ACCESS_OTHER)
data = {**self.data, "do_send_registration_email": True}
res = self.assertValidPostResponse(data=data, redirect_url=self.base_url)
logs = LogEntry.objects.all()
diff --git a/membership_file/tests/tests_admin.py b/membership_file/tests/tests_admin.py
index 71d20955..b22cb529 100644
--- a/membership_file/tests/tests_admin.py
+++ b/membership_file/tests/tests_admin.py
@@ -227,7 +227,6 @@ def setUp(self):
"student_number": "",
"educational_institution": "TU/e",
"external_card_digits": "",
- "external_card_cluster": "",
"email": self.email,
"street": "De Lampendriessen",
"house_number": "31",
diff --git a/membership_file/tests/tests_model.py b/membership_file/tests/tests_model.py
index 44ce1713..fe52b14c 100644
--- a/membership_file/tests/tests_model.py
+++ b/membership_file/tests/tests_model.py
@@ -138,8 +138,11 @@ def test_is_active_no_active_years(self):
class RoomModelTest(TestCase):
# Tests the display method of Room
def test_room_display(self):
- room = Room(id=2, name="Basement", access="Keycard")
- self.assertEqual(str(room), "Basement (Keycard)")
+ room = Room(id=2, name="Basement", access_type=Room.ACCESS_OTHER, access_specification="Keycard")
+ self.assertEqual(str(room), "Basement (Other - Keycard)")
+
+ room = Room(id=2, name="Basement", access_type=Room.ACCESS_KEY, room_number="1.042")
+ self.assertEqual(str(room), "1.042 - Basement (Key)")
class MemberYearTest(TestCase):
diff --git a/membership_file/tests/tests_user.py b/membership_file/tests/tests_user.py
index bc7a1468..12be98b5 100644
--- a/membership_file/tests/tests_user.py
+++ b/membership_file/tests/tests_user.py
@@ -127,26 +127,17 @@ def test_external_card_number(self):
# Display None if there is no card number
self.member_to_run_tests_on.external_card_number = None
self.member_to_run_tests_on.external_card_digits = None
- self.member_to_run_tests_on.external_card_cluster = None
self.assertIsNone(self.member_to_run_tests_on.display_external_card_number())
# Display 1234567-123 if they exist
self.member_to_run_tests_on.external_card_number = "1234567"
self.member_to_run_tests_on.external_card_digits = "980"
- self.member_to_run_tests_on.external_card_cluster = None
self.assertEqual("1234567-980", self.member_to_run_tests_on.display_external_card_number())
- # Display 1234567 (Cluster) if they exist
+ # Display 1234567 if they exist
self.member_to_run_tests_on.external_card_number = "1234567"
self.member_to_run_tests_on.external_card_digits = None
- self.member_to_run_tests_on.external_card_cluster = "K. Nights"
- self.assertEqual("1234567 (K. Nights)", self.member_to_run_tests_on.display_external_card_number())
-
- # Display 1234567-980 (Cluster) if they exist
- self.member_to_run_tests_on.external_card_number = "1234567"
- self.member_to_run_tests_on.external_card_digits = "980"
- self.member_to_run_tests_on.external_card_cluster = "K. Nights"
- self.assertEqual("1234567-980 (K. Nights)", self.member_to_run_tests_on.display_external_card_number())
+ self.assertEqual("1234567", self.member_to_run_tests_on.display_external_card_number())
# Tests the address display method
def test_address(self):
diff --git a/user_interaction/static/themes/april-theme.js b/user_interaction/static/themes/april-theme.js
index 9ea2427c..4cb60364 100644
--- a/user_interaction/static/themes/april-theme.js
+++ b/user_interaction/static/themes/april-theme.js
@@ -1,11 +1,11 @@
// Modify the DOM
$(document).ready(function () {
/****************************************************************
- * Generate Navbar Rickroll
+ * Generate Navbar Knights rap
*****************************************************************/
$(".navbar-nav.mr-auto").append(`
- Lustrum
+ Lustrum
`)
diff --git a/user_interaction/views.py b/user_interaction/views.py
index 112cdccf..0c49931a 100644
--- a/user_interaction/views.py
+++ b/user_interaction/views.py
@@ -78,6 +78,14 @@ class April2023LiveStreamView(MembershipRequiredMixin, TemplateView):
"The Knights of the Kitchen Table are the best, largest, and only boardgame and roleplay association in Eindhoven!",
"Frambozen bestaan",
"This message is sponsored by Fantasy Court",
+ "Fun fact: The boardgame committee's favourite boardgame is Fortnite Monopoly!",
+ "Fun fact: The roleplaying committee's favourite system is the Rolepearing Game!",
+ "Fun fact: The Ivoren Wachter's favourite swordfighting-move is the shield bash!",
+ "Fun fact: The cooking committee's favourite dish is Soep Ultiem!",
+ "Fun fact: Fantasy Court's favourite vendor is Conferences!",
+ "Fun fact: The activity committee's favourite activity is drinking away sorrows in Hubble!",
+ "Fun fact: The board's favourite board is board 18!",
+ "The Knights have their own alumni association: The Lords of the Kitchen Table. If you're old, message the board!",
]