diff --git a/myhpi/core/markdown/extensions.py b/myhpi/core/markdown/extensions.py
index 15ec98e9..2e51d4da 100644
--- a/myhpi/core/markdown/extensions.py
+++ b/myhpi/core/markdown/extensions.py
@@ -75,7 +75,7 @@ def breakify(self, match):
)
-class QuorumPrepocessor(MinutesBasePreprocessor):
+class QuorumPreprocessor(MinutesBasePreprocessor):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.patterns = [
@@ -138,11 +138,11 @@ class HeadingLevelPreprocessor(MinutesBasePreprocessor):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.patterns = [
- (r"^#{1,5}", self.decrease),
+ (r"^#{1,5} ", self.decrease),
]
def decrease(self, match):
- return f"{match.group(0)}#"
+ return f"#{match.group(0)}"
class InternalLinkPattern(LinkInlineProcessor):
@@ -158,6 +158,9 @@ def handleMatch(self, m, data=None):
def url(self, id):
return Page.objects.get(id=id).localized.get_url()
+ def default_pattern():
+ return r"\[(?P
[^\[]+)\]\(page:(?P\d+)\)"
+
class ImagePattern(LinkInlineProcessor):
def handleMatch(self, m, data=None):
@@ -174,6 +177,9 @@ def handleMatch(self, m, data=None):
def url(self, id):
return Image.objects.get(id=id).get_rendition("width-800").url
+ def default_pattern():
+ return r"!\[(?P[^\[]+)\]\(image:(?P\d+)\)"
+
class MinuteExtension(Extension):
def extendMarkdown(self, md):
@@ -181,16 +187,16 @@ def extendMarkdown(self, md):
md.preprocessors.register(VotePreprocessor(md), "votify", 200)
md.preprocessors.register(StartEndPreprocessor(md), "start_or_endify", 200)
md.preprocessors.register(BreakPreprocessor(md), "breakify", 200)
- md.preprocessors.register(QuorumPrepocessor(md), "quorumify", 200)
+ md.preprocessors.register(QuorumPreprocessor(md), "quorumify", 200)
md.preprocessors.register(EnterLeavePreprocessor(md), "enter_or_leavify", 200)
md.preprocessors.register(HeadingLevelPreprocessor(md), "decrease", 200)
md.inlinePatterns.register(
- InternalLinkPattern(r"\[(?P[^\[]+)\]\(page:(?P\d+)\)", md),
+ InternalLinkPattern(InternalLinkPattern.default_pattern(), md),
"InternalLinkPattern",
200,
)
md.inlinePatterns.register(
- ImagePattern(r"!\[(?P[^\[]+)\]\(image:(?P\d+)\)", md),
+ ImagePattern(ImagePattern.default_pattern(), md),
"ImagePattern",
200,
)
diff --git a/myhpi/polls/models.py b/myhpi/polls/models.py
index 317a89ba..d43680d4 100644
--- a/myhpi/polls/models.py
+++ b/myhpi/polls/models.py
@@ -65,18 +65,25 @@ def serve(self, request, *args, **kwargs):
elif request.method == "POST" and self.can_vote(request.user):
choices = request.POST.getlist("choice")
if len(choices) == 0:
- messages.error(request, "You must at least select one choice.")
+ messages.error(request, "You must select at least one choice.")
elif len(choices) > self.max_allowed_answers:
messages.error(
request,
"You can only select up to {} options.".format(self.max_allowed_answers),
)
else:
+ confirmed_choices = 0
for choice_id in choices:
- choice = self.choices.filter(id=choice_id)
- choice.update(votes=F("votes") + 1)
- self.participants.add(request.user)
- messages.success(request, "Your vote has been counted.")
+ choice = self.choices.filter(id=choice_id).first()
+ if choice and choice.page == self:
+ choice.votes += 1
+ choice.save()
+ confirmed_choices += 1
+ else:
+ messages.error(request, "Invalid choice.")
+ if confirmed_choices > 0:
+ self.participants.add(request.user)
+ messages.success(request, "Your vote has been counted.")
return redirect(self.relative_url(self.get_site()))
return super().serve(request, *args, **kwargs)
diff --git a/myhpi/tests/core/setup.py b/myhpi/tests/core/setup.py
index eb0eb575..f13e71f0 100644
--- a/myhpi/tests/core/setup.py
+++ b/myhpi/tests/core/setup.py
@@ -1,5 +1,6 @@
-from django.contrib.auth.models import Group, User
-from wagtail.models import Site
+from django.contrib.auth.models import Group, Permission, User
+from wagtail.documents.models import Document
+from wagtail.models import Collection, GroupCollectionPermission, Site
from myhpi.core.models import (
FirstLevelMenuItem,
@@ -41,7 +42,7 @@ def create_users():
def create_groups(users):
superuser, student, student_representative = users
- students = Group.objects.create(name="Students")
+ students = Group.objects.create(name="Student")
fsr = Group.objects.create(name="Student Representative Group")
students.user_set.add(superuser)
@@ -228,12 +229,70 @@ def setup_minutes(group, students_group, parent, user):
return minutes, minutes_list
+def create_collections(groups):
+ root_collection = Collection.get_first_root_node()
+ for group in groups:
+ group_collection = root_collection.add_child(name=f"{group.name} collection")
+ group_collection.save()
+ GroupCollectionPermission.objects.create(
+ group=group,
+ collection=group_collection,
+ permission=Permission.objects.get(
+ content_type__app_label="wagtaildocs", codename="add_document"
+ ),
+ )
+ GroupCollectionPermission.objects.create(
+ group=group,
+ collection=group_collection,
+ permission=Permission.objects.get(
+ content_type__app_label="wagtaildocs", codename="change_document"
+ ),
+ )
+ GroupCollectionPermission.objects.create(
+ group=group,
+ collection=group_collection,
+ permission=Permission.objects.get(
+ content_type__app_label="wagtaildocs", codename="choose_document"
+ ),
+ )
+ yield group_collection
+
+
+def get_test_image_file():
+ from django.core.files.uploadedfile import SimpleUploadedFile
+
+ image_file = SimpleUploadedFile(
+ name="test_image.jpg",
+ content=open("myhpi/tests/files/test_image.jpg", "rb").read(),
+ content_type="image/jpeg",
+ )
+ return image_file
+
+
+def create_documents(collections):
+ documents = [
+ Document.objects.create(
+ title="First document",
+ file=get_test_image_file(),
+ collection=collections[0],
+ ),
+ Document.objects.create(
+ title="Second document",
+ file=get_test_image_file(),
+ collection=collections[1],
+ ),
+ ]
+ return documents
+
+
def setup_data():
users = create_users()
groups = create_groups(users)
basic_pages = create_basic_page_structure()
information_pages = create_information_pages(groups, basic_pages["information_menu"])
minutes, minutes_list = setup_minutes(groups[1], groups[0], basic_pages["fsr_menu"], users[2])
+ collections = list(create_collections(groups))
+ documents = create_documents(collections)
return {
"basic_pages": basic_pages,
@@ -242,4 +301,6 @@ def setup_data():
"pages": information_pages,
"minutes": minutes,
"minutes_list": minutes_list,
+ "collections": collections,
+ "documents": documents,
}
diff --git a/myhpi/tests/core/test_auth.py b/myhpi/tests/core/test_auth.py
new file mode 100644
index 00000000..d81e7b4b
--- /dev/null
+++ b/myhpi/tests/core/test_auth.py
@@ -0,0 +1,49 @@
+from myhpi.core.auth import MyHPIOIDCAB
+from myhpi.tests.core.utils import MyHPIPageTestCase
+
+
+class AuthTests(MyHPIPageTestCase):
+ def setUp(self):
+ super().setUp()
+ self.auth_backend = MyHPIOIDCAB()
+
+ def test_create_user(self):
+ claims = {
+ "email": "ali.gator@example.org",
+ "given_name": "Ali",
+ "family_name": "Gator",
+ "sub": "ali.gator",
+ }
+ user = self.auth_backend.create_user(claims)
+ self.assertEqual(user.username, "ali.gator")
+ self.assertFalse(user.groups.filter(name="Student").exists())
+
+ matching_users = self.auth_backend.filter_users_by_claims(claims)
+ self.assertEqual(len(matching_users), 1)
+
+ def test_create_student(self):
+ claims = {
+ "email": "grace.hopper@student.uni-potsdam.example.com",
+ "given_name": "Grace",
+ "family_name": "Hopper",
+ "sub": "grace.hopper",
+ }
+ user = self.auth_backend.create_user(claims)
+ self.assertEqual(user.username, "grace.hopper")
+ self.assertEqual(user.email, "grace.hopper@student.example.com")
+ self.assertTrue(user.groups.filter(name="Student").exists())
+
+ def test_update_user(self):
+ claims = {
+ "email": "jw.goethe@weimar.de",
+ "given_name": "Johann Wolfgang",
+ "family_name": "Goethe",
+ "sub": "jw.goethe",
+ }
+ user = self.auth_backend.create_user(claims)
+ self.assertEqual(user.username, "jw.goethe")
+ self.assertEqual(user.last_name, "Goethe")
+ claims["family_name"] = "von Goethe"
+ user = self.auth_backend.update_user(user, claims)
+ self.assertEqual(user.first_name, "Johann Wolfgang")
+ self.assertEqual(user.last_name, "von Goethe")
diff --git a/myhpi/tests/core/test_email_utils.py b/myhpi/tests/core/test_email_utils.py
new file mode 100644
index 00000000..5ac5d74f
--- /dev/null
+++ b/myhpi/tests/core/test_email_utils.py
@@ -0,0 +1,41 @@
+from unittest import TestCase
+
+from myhpi import settings
+from myhpi.core.utils import (
+ alternative_emails,
+ email_belongs_to_domain,
+ replace_email_domain,
+ toggle_institution,
+)
+
+
+class EmailUtilTest(TestCase):
+ def test_email_belongs_to_domain(self):
+ emails = ["abc@example.com", "abc@myhpi.de"]
+ domains = ["example.com", "myhpi.de"]
+ for email, domain in zip(emails, domains):
+ self.assertTrue(email_belongs_to_domain(email, domain))
+ self.assertFalse(email_belongs_to_domain(emails[0], domains[1]))
+
+ def test_replace_email_domain(self):
+ email = "abc@example.com"
+ original_domain = "example.com"
+ new_domain = "myhpi.de"
+ self.assertEqual(replace_email_domain(email, original_domain, new_domain), "abc@myhpi.de")
+
+ def test_toggle_institution(self):
+ emails = ["user1@hpi.uni-potsdam.de", "user2@unrelated.com", "user3@hpi.de"]
+ expected = ["user1@hpi.de", "user2@unrelated.com", "user3@hpi.uni-potsdam.de"]
+ for email, expected_email in zip(emails, expected):
+ toggled = list(toggle_institution(email))
+ if not "unrelated" in email:
+ self.assertEqual(toggled[0], expected_email)
+
+ def test_alternative_emails(self):
+ email = "user@hpi.de"
+ alternatives = [
+ "user@hpi.uni-potsdam.de",
+ "user@student.hpi.de",
+ "user@student.hpi.uni-potsdam.de",
+ ]
+ self.assertSetEqual(set(alternative_emails(email)), set(alternatives))
diff --git a/myhpi/tests/core/test_polls.py b/myhpi/tests/core/test_polls.py
new file mode 100644
index 00000000..5b844ed7
--- /dev/null
+++ b/myhpi/tests/core/test_polls.py
@@ -0,0 +1,105 @@
+from datetime import datetime, timedelta
+
+from myhpi.polls.models import Poll, PollChoice, PollList
+from myhpi.tests.core.utils import MyHPIPageTestCase
+
+
+class PollTests(MyHPIPageTestCase):
+ def setUp(self):
+ super().setUp()
+ self.poll_list = PollList(
+ title="Polls",
+ slug="polls",
+ path="0001000200010005",
+ depth=4,
+ is_public=True,
+ )
+ self.information_menu.add_child(instance=self.poll_list)
+
+ self.poll = Poll(
+ title="How are you?",
+ slug="how-are-you",
+ question="How are you?",
+ description="This is a poll to check how you are.",
+ start_date=datetime.now() - timedelta(days=1),
+ end_date=datetime.now() + timedelta(days=1),
+ max_allowed_answers=1,
+ results_visible=False,
+ is_public=True,
+ )
+
+ self.poll_list.add_child(instance=self.poll)
+
+ self.choice_good = PollChoice(
+ text="Good",
+ page=self.poll,
+ votes=0,
+ )
+ self.choice_good.save()
+ self.choice_bad = PollChoice(
+ text="Bad",
+ page=self.poll,
+ votes=0,
+ )
+ self.choice_bad.save()
+
+ def test_can_vote_once(self):
+ self.sign_in_as_student()
+ self.assertTrue(self.poll.can_vote(self.student))
+ self.poll.participants.add(self.student)
+ self.assertFalse(self.poll.can_vote(self.student))
+
+ def test_post_vote(self):
+ self.sign_in_as_student()
+ self.assertTrue(self.poll.can_vote(self.student))
+ self.client.post(
+ self.poll.url,
+ data={"choice": [self.choice_good.id]},
+ )
+ self.choice_good.refresh_from_db()
+ self.assertEqual(self.choice_good.votes, 1)
+ self.assertEqual(self.choice_good.percentage(), 100)
+ self.assertEqual(self.choice_bad.percentage(), 0)
+ self.assertFalse(self.poll.can_vote(self.student))
+
+ def test_post_vote_invalid_choice(self):
+ self.sign_in_as_student()
+ self.assertTrue(self.poll.can_vote(self.student))
+ self.client.post(
+ self.poll.url,
+ data={"choice": [self.choice_good.id + 9999]},
+ )
+ self.choice_good.refresh_from_db()
+ self.assertEqual(self.choice_good.votes, 0)
+ self.assertTrue(self.poll.can_vote(self.student))
+
+ def test_post_vote_no_choice(self):
+ self.sign_in_as_student()
+ self.assertTrue(self.poll.can_vote(self.student))
+ response = self.client.post(
+ self.poll.url,
+ data={"choice": []},
+ )
+ self.assertContains(response, "You must select at least one choice.")
+ self.assertTrue(self.poll.can_vote(self.student))
+
+ def test_post_vote_too_many_choices(self):
+ self.sign_in_as_student()
+ self.assertTrue(self.poll.can_vote(self.student))
+ response = self.client.post(
+ self.poll.url,
+ data={"choice": [self.choice_good.id, self.choice_bad.id]},
+ )
+ self.assertContains(response, "You can only select up to 1 options.", 1)
+ self.assertTrue(self.poll.can_vote(self.student))
+
+ def test_post_vote_before_start_date(self):
+ self.sign_in_as_student()
+ self.poll.start_date = datetime.now() + timedelta(days=1)
+ self.poll.save()
+ self.assertTrue(self.poll.can_vote(self.student))
+ response = self.client.post(
+ self.poll.url, data={"choice": [self.choice_good.id]}, follow=True
+ )
+ self.assertContains(response, "This poll has not yet started.")
+ self.assertTrue(self.poll.can_vote(self.student))
diff --git a/myhpi/tests/core/test_view_permissions.py b/myhpi/tests/core/test_view_permissions.py
index 5164424d..3125d500 100644
--- a/myhpi/tests/core/test_view_permissions.py
+++ b/myhpi/tests/core/test_view_permissions.py
@@ -52,3 +52,16 @@ def test_super_user_can_view_all_pages(self):
self.assertEqual(response.status_code, 200)
response = self.client.get(self.private_page.url, follow=True)
self.assertEqual(response.status_code, 200)
+
+ def test_document_view(self):
+ self.common_page.attachments.add(self.first_document)
+ self.common_page.save()
+ self.private_page.attachments.add(self.second_document)
+ self.private_page.save()
+
+ self.sign_in_as_student()
+ response = self.client.get(self.first_document.url, follow=True)
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.get(self.second_document.url, follow=True)
+ self.assertEqual(response.status_code, 403)
diff --git a/myhpi/tests/core/test_widgets.py b/myhpi/tests/core/test_widgets.py
new file mode 100644
index 00000000..85e69e73
--- /dev/null
+++ b/myhpi/tests/core/test_widgets.py
@@ -0,0 +1,28 @@
+from dataclasses import dataclass
+
+from django.forms.models import ModelChoiceIteratorValue
+
+from myhpi.core.widgets import AttachmentSelectWidget
+from myhpi.tests.core.utils import MyHPIPageTestCase
+
+
+class WidgetTests(MyHPIPageTestCase):
+ def setUp(self):
+ super().setUp()
+
+ def test_attachment_select_widget(self):
+ self.sign_in_as_student()
+ # student has access to document 1, not document 2
+ choices = list(
+ map(
+ lambda doc: (ModelChoiceIteratorValue(doc.id, doc), doc.title),
+ self.test_data["documents"],
+ )
+ )
+ widget = AttachmentSelectWidget(user=self.student, choices=choices)
+ optgroups = widget.optgroups("attachments", [])
+ self.assertEqual(len(optgroups), 1)
+
+ widget = AttachmentSelectWidget(user=self.student_representative, choices=choices)
+ optgroups = widget.optgroups("attachments", [])
+ self.assertEqual(len(optgroups), 2)
diff --git a/myhpi/tests/core/utils.py b/myhpi/tests/core/utils.py
index a7f03af4..396dbf48 100644
--- a/myhpi/tests/core/utils.py
+++ b/myhpi/tests/core/utils.py
@@ -27,6 +27,9 @@ def setUp(self):
self.student = self.test_data["users"][1]
self.student_representative = self.test_data["users"][2]
+ self.first_document = self.test_data["documents"][0]
+ self.second_document = self.test_data["documents"][1]
+
def sign_in_as_student(self):
self.client.force_login(self.student)
diff --git a/myhpi/tests/files/test_image.jpg b/myhpi/tests/files/test_image.jpg
new file mode 100644
index 00000000..056b24f2
Binary files /dev/null and b/myhpi/tests/files/test_image.jpg differ
diff --git a/myhpi/tests/test_markdown_extensions.py b/myhpi/tests/test_markdown_extensions.py
new file mode 100644
index 00000000..2a418063
--- /dev/null
+++ b/myhpi/tests/test_markdown_extensions.py
@@ -0,0 +1,130 @@
+import re
+from typing import Collection
+
+import django
+from django.test import TestCase
+from django.utils.translation import activate
+from wagtail.core.models import Page
+from wagtail.images.models import Image
+
+from myhpi.core.markdown.extensions import ImagePattern
+
+django.setup()
+
+from myhpi.core.markdown.extensions import (
+ BreakPreprocessor,
+ EnterLeavePreprocessor,
+ HeadingLevelPreprocessor,
+ InternalLinkPattern,
+ QuorumPreprocessor,
+ StartEndPreprocessor,
+ VotePreprocessor,
+)
+
+
+class TestMarkdownExtensions(TestCase):
+ def test_vote_preprocessor(self):
+ vp = VotePreprocessor()
+ text = ["[2|3|4]", "[a|b|c]"] # Second line is not processed
+ result = vp.run(text)
+ self.assertEqual(result, ["**[2|3|4]**", "[a|b|c]"])
+
+ def test_start_end_preprocessor(self):
+ activate("en")
+ sep = StartEndPreprocessor()
+ text = ["|start|(12:00)", "|end|(16:00)"]
+ result = sep.run(text)
+ self.assertEqual(result, ["*Begin of meeting: 12:00* ", "*End of meeting: 16:00* "])
+
+ def test_break_preprocessor(self):
+ activate("en")
+ bp = BreakPreprocessor()
+ text = ["|break|(12:00)(13:00)"]
+ result = bp.run(text)
+ self.assertEqual(result, ["*Meeting break: 12:00 – 13:00*"])
+
+ def test_quorum_preprocessor(self):
+ activate("en")
+ qp = QuorumPreprocessor()
+ text = ["|quorum|(3/8)", "|quorum|(4/8)"]
+ result = qp.run(text)
+ self.assertEqual(result, ["*3/8 present → not quorate* ", "*4/8 present → quorate* "])
+
+ def test_enter_leave_preprocessor(self):
+ activate("en")
+ elp = EnterLeavePreprocessor()
+ text = [
+ "|enter|(12:00)(First Last)",
+ "|enter|(12:00)(Prof. First Last)",
+ "|enter|(12:00)(Prof. First Last)(Means)",
+ "|leave|(12:00)(Prof. First Last)",
+ ]
+ result = elp.run(text)
+ self.assertEqual(
+ result,
+ [
+ "*12:00: First Last enters the meeting* ",
+ "*12:00: Prof. First Last enters the meeting* ",
+ "*12:00: Prof. First Last enters the meeting via Means* ",
+ "*12:00: Prof. First Last leaves the meeting* ",
+ ],
+ )
+
+ def test_heading_level_preprocessor(self):
+ hlp = HeadingLevelPreprocessor()
+ text = [
+ "# Heading 1",
+ "## Heading 2",
+ "### Heading 3",
+ "#### Heading 4",
+ "##### Heading 5",
+ "###### Heading 6",
+ ]
+ result = hlp.run(text)
+ self.assertEqual(
+ result,
+ [
+ "## Heading 1",
+ "### Heading 2",
+ "#### Heading 3",
+ "##### Heading 4",
+ "###### Heading 5",
+ "###### Heading 6",
+ ],
+ )
+
+ def test_internal_link_preprocessor(self):
+ ilp = InternalLinkPattern(InternalLinkPattern.default_pattern())
+
+ from myhpi.tests.core.setup import setup_data
+
+ test_data = setup_data()
+ test_page = test_data["pages"][0]
+ text = f"[Page title](page:{test_page.id})"
+ el, _, _ = ilp.handleMatch(re.match(ilp.pattern, text))
+ self.assertEqual(el.attrib["href"], test_page.localized.get_url())
+
+ def test_image_pattern(self):
+ activate("en")
+ from django.core.files.uploadedfile import SimpleUploadedFile
+
+ ip = ImagePattern(ImagePattern.default_pattern())
+
+ image_file = SimpleUploadedFile(
+ name="test_image.jpg",
+ content=open("myhpi/tests/files/test_image.jpg", "rb").read(),
+ content_type="image/jpeg",
+ )
+
+ image = Image.objects.create(
+ title="Test image",
+ file=image_file,
+ )
+
+ text = f"![Alt text](image:{image.id})"
+ invalid_text = "![Alt text](image:1234567890)"
+ el, _, _ = ip.handleMatch(re.match(ip.pattern, text))
+ self.assertEqual(el.attrib["src"], image.get_rendition("width-800").url)
+
+ el, _, _ = ip.handleMatch(re.match(ip.pattern, invalid_text))
+ self.assertEqual(el.text, "[missing image]")
diff --git a/myhpi/tests/test_minutes_extensions.py b/myhpi/tests/test_minutes_extensions.py
deleted file mode 100644
index 0bb0e118..00000000
--- a/myhpi/tests/test_minutes_extensions.py
+++ /dev/null
@@ -1,29 +0,0 @@
-import django
-from django.test import TestCase
-from django.utils.translation import activate
-
-django.setup()
-
-from myhpi.core.markdown.extensions import EnterLeavePreprocessor
-
-
-class TestMinuteExtensions(TestCase):
- def test_enter(self):
- activate("en")
- elp = EnterLeavePreprocessor()
- text = [
- "|enter|(12:00)(First Last)",
- "|enter|(12:00)(Prof. First Last)",
- "|enter|(12:00)(Prof. First Last)(Means)",
- "|leave|(12:00)(Prof. First Last)",
- ]
- result = elp.run(text)
- self.assertEqual(
- result,
- [
- "*12:00: First Last enters the meeting* ",
- "*12:00: Prof. First Last enters the meeting* ",
- "*12:00: Prof. First Last enters the meeting via Means* ",
- "*12:00: Prof. First Last leaves the meeting* ",
- ],
- )