From 1bfce28948cca62048afcb3e5bdde4b98e729c52 Mon Sep 17 00:00:00 2001 From: frcroth Date: Thu, 9 Nov 2023 21:26:12 +0100 Subject: [PATCH 1/6] Use tomselect --- myhpi/core/models.py | 15 ++++--------- myhpi/settings.py | 2 +- myhpi/urls.py | 3 ++- poetry.lock | 52 ++++++++++++++------------------------------ pyproject.toml | 2 +- 5 files changed, 24 insertions(+), 50 deletions(-) diff --git a/myhpi/core/models.py b/myhpi/core/models.py index 34e529c6..b9180f5c 100644 --- a/myhpi/core/models.py +++ b/myhpi/core/models.py @@ -6,7 +6,7 @@ from django.db import models from django.db.models import BooleanField, CharField, DateField, ForeignKey, Model, Q from django.http import HttpResponseRedirect -from django_select2 import forms as s2forms +from django_tomselect.widgets import TomSelectWidget, TomSelectTabularWidget, TomSelectMultipleWidget from modelcluster.contrib.taggit import ClusterTaggableManager from modelcluster.fields import ParentalKey, ParentalManyToManyField from taggit.models import ItemBase, TagBase @@ -170,13 +170,6 @@ def get_last_minutes(self): return existing_minutes.last().specific -class UserSelectWidget(s2forms.ModelSelect2MultipleWidget): - search_fields = [ - "username__icontains", - "email__icontains", - ] - - class Minutes(BasePage): date = DateField() moderator = ForeignKey( @@ -193,9 +186,9 @@ class Minutes(BasePage): content_panels = Page.content_panels + [ FieldPanel("date"), - FieldPanel("moderator"), - FieldPanel("author"), - FieldPanel("participants", widget=UserSelectWidget), + FieldPanel("moderator", widget=TomSelectWidget(label_field="username")), + FieldPanel("author", widget=TomSelectWidget(label_field="username")), + FieldPanel("participants", widget=TomSelectMultipleWidget(label_field="username")), FieldPanel("labels"), FieldPanel("body"), FieldPanel("guests"), diff --git a/myhpi/settings.py b/myhpi/settings.py index 3baacd04..f8759fb2 100644 --- a/myhpi/settings.py +++ b/myhpi/settings.py @@ -41,7 +41,7 @@ "django.contrib.staticfiles", "django.contrib.messages", "django_bootstrap_icons", - "django_select2", + "django_tomselect", "modelcluster", "mozilla_django_oidc", "taggit", diff --git a/myhpi/urls.py b/myhpi/urls.py index 2e4e5c3b..5aec85e3 100644 --- a/myhpi/urls.py +++ b/myhpi/urls.py @@ -6,6 +6,7 @@ from django.contrib.auth import views as auth_views from django.urls import include, path, reverse_lazy from django.views.generic import RedirectView +from django_tomselect.views import AutocompleteView from wagtail.admin import urls as wagtailadmin_urls from wagtail.core import urls as wagtail_urls from wagtail.documents import urls as wagtaildocs_urls @@ -25,7 +26,7 @@ ), name="login", ), - path("select2/", include("django_select2.urls")), + path("autocomplete/", AutocompleteView.as_view(), name="autocomplete"), path("__debug__/", include("debug_toolbar.urls")), path( ".well-known/security.txt", diff --git a/poetry.lock b/poetry.lock index edba7a54..81c5673f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. [[package]] name = "anyascii" @@ -457,20 +457,6 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""} argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] -[[package]] -name = "django-appconf" -version = "1.0.5" -description = "A helper class for handling configuration defaults of packaged apps gracefully." -optional = false -python-versions = ">=3.6" -files = [ - {file = "django-appconf-1.0.5.tar.gz", hash = "sha256:be3db0be6c81fa84742000b89a81c016d70ae66a7ccb620cdef592b1f1a6aaa4"}, - {file = "django_appconf-1.0.5-py3-none-any.whl", hash = "sha256:ae9f864ee1958c815a965ed63b3fba4874eec13de10236ba063a788f9a17389d"}, -] - -[package.dependencies] -django = "*" - [[package]] name = "django-bootstrap-icons" version = "0.8.3" @@ -566,26 +552,6 @@ Django = "*" [package.extras] testing = ["django-modelcluster"] -[[package]] -name = "django-select2" -version = "8.1.2" -description = "This is a Django_ integration of Select2_." -optional = false -python-versions = ">=3.8" -files = [ - {file = "django_select2-8.1.2-py3-none-any.whl", hash = "sha256:dc09e09299988dd7cc1f31c27582976377f0e1479101be1f3bb0d1f24216477f"}, - {file = "django_select2-8.1.2.tar.gz", hash = "sha256:f44685ee1c39090aade01e3ebc256702f05620f3c78a3c268440ad9a66070876"}, -] - -[package.dependencies] -django = ">=3.2" -django-appconf = ">=0.6.0" - -[package.extras] -docs = ["sphinx"] -selenium = ["selenium"] -test = ["pytest", "pytest-cov", "pytest-django", "selenium"] - [[package]] name = "django-static-precompiler" version = "2.4" @@ -619,6 +585,20 @@ files = [ [package.dependencies] Django = ">=2.2" +[[package]] +name = "django-tomselect" +version = "0.4.4" +description = "Django autocomplete widgets and views using Tom Select" +optional = false +python-versions = ">=3.8" +files = [ + {file = "django-tomselect-0.4.4.tar.gz", hash = "sha256:e11acc5cb93e9e254c06ce2283fd9f17f326b42be51feb53235ded5e7472323b"}, + {file = "django_tomselect-0.4.4-py3-none-any.whl", hash = "sha256:0052cd38112e7621617f7af82aa492c15086047c049706e238305bb4e0dfa459"}, +] + +[package.dependencies] +Django = "*" + [[package]] name = "django-treebeard" version = "4.5.1" @@ -1762,4 +1742,4 @@ pgsql = [] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "1fa142f38a5a71b798119f6b7a365e73d2eb36f16a6db8bec26f1477075eb139" +content-hash = "36c85987fdccd0605cc370cf1758219e6a9e23d4e90f0f217159f680c0a876df" diff --git a/pyproject.toml b/pyproject.toml index faa0c9a5..b313dbf7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,6 @@ django-environ = "^0.10.0" wagtail-localize = "^1.4" mozilla-django-oidc = "^3.0.0" django-bootstrap-icons = "^0.8.3" -django-select2 = "^8.1.2" django-static-precompiler = {extras = ["libsass"], version = "^2.4"} django-debug-toolbar = "^4.2.0" django-permissionedforms = "^0.1" @@ -20,6 +19,7 @@ tenca = "^0.0.2" html2text = "^2020.1.16" wagtail-markdown = "^0.10.0" autoflake = "^2.2.1" +django-tomselect = "^0.4.4" [tool.poetry.group.dev.dependencies] pylint = "^2.17.5" From 770f73910bb1c13525e6c01c9654bfd56a87b8e1 Mon Sep 17 00:00:00 2001 From: frcroth Date: Thu, 9 Nov 2023 21:27:24 +0100 Subject: [PATCH 2/6] Lint --- myhpi/core/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/myhpi/core/models.py b/myhpi/core/models.py index b9180f5c..e51b64b8 100644 --- a/myhpi/core/models.py +++ b/myhpi/core/models.py @@ -6,7 +6,7 @@ from django.db import models from django.db.models import BooleanField, CharField, DateField, ForeignKey, Model, Q from django.http import HttpResponseRedirect -from django_tomselect.widgets import TomSelectWidget, TomSelectTabularWidget, TomSelectMultipleWidget +from django_tomselect.widgets import TomSelectMultipleWidget, TomSelectWidget from modelcluster.contrib.taggit import ClusterTaggableManager from modelcluster.fields import ParentalKey, ParentalManyToManyField from taggit.models import ItemBase, TagBase From 94a46a18b273ed23a5187b6e75a5f3f9e26ca60e Mon Sep 17 00:00:00 2001 From: frcroth Date: Tue, 21 Nov 2023 20:20:34 +0100 Subject: [PATCH 3/6] Add user profile to get display name field for tomselect --- ...09_userprofile_for_minutes_display_name.py | 72 +++++++++++++++++++ myhpi/core/models.py | 36 ++++++++-- 2 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 myhpi/core/migrations/0009_userprofile_for_minutes_display_name.py diff --git a/myhpi/core/migrations/0009_userprofile_for_minutes_display_name.py b/myhpi/core/migrations/0009_userprofile_for_minutes_display_name.py new file mode 100644 index 00000000..d36fc735 --- /dev/null +++ b/myhpi/core/migrations/0009_userprofile_for_minutes_display_name.py @@ -0,0 +1,72 @@ +# Generated by Django 4.0.7 on 2023-11-21 19:05 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import modelcluster.fields + + +def create_user_profiles(apps, schema_editor): + User = apps.get_model("auth", "User") + for user in User.objects.all(): + UserProfile = apps.get_model("core", "UserProfile") + display_name = user.first_name + " " + user.last_name + UserProfile.objects.create(user=user, display_name=display_name) + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("core", "0008_remove_footer_column_4"), + ] + + operations = [ + migrations.CreateModel( + name="UserProfile", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("display_name", models.CharField(blank=True, max_length=255)), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), + ), + ], + ), + migrations.AlterField( + model_name="minutes", + name="author", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="author", + to="core.userprofile", + ), + ), + migrations.AlterField( + model_name="minutes", + name="moderator", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="moderator", + to="core.userprofile", + ), + ), + migrations.AlterField( + model_name="minutes", + name="participants", + field=modelcluster.fields.ParentalManyToManyField( + related_name="minutes", to="core.userprofile" + ), + ), + migrations.RunPython(create_user_profiles), + ] diff --git a/myhpi/core/models.py b/myhpi/core/models.py index d7b9775d..1da858c4 100644 --- a/myhpi/core/models.py +++ b/myhpi/core/models.py @@ -1,6 +1,9 @@ from collections import defaultdict from datetime import date +from django.db.models import CharField, Value, F +from django.db.models.functions import Concat + from django import forms from django.contrib.auth.models import Group, User from django.db import models @@ -22,6 +25,27 @@ from myhpi.core.widgets import AttachmentSelectWidget +class UserProfile(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE) + display_name = models.CharField(max_length=255, blank=True) + + def __str__(self): + return self.display_name + + def get_full_name(self): + return self.display_name + + +from django.db.models.signals import post_save +from django.dispatch import receiver + + +@receiver(post_save, sender=User) +def post_user_save(sender, instance, created, **kwargs): + display_name = instance.first_name + " " + instance.last_name + UserProfile.objects.find(user=instance).update_or_create(display_name=display_name) + + class BasePage(Page): visible_for = ParentalManyToManyField(Group, blank=True, related_name="visible_basepages") is_public = BooleanField() @@ -173,12 +197,12 @@ def get_last_minutes(self): class Minutes(BasePage): date = DateField() moderator = ForeignKey( - User, blank=True, null=True, on_delete=models.PROTECT, related_name="moderator" + UserProfile, blank=True, null=True, on_delete=models.PROTECT, related_name="moderator" ) author = ForeignKey( - User, blank=True, null=True, on_delete=models.PROTECT, related_name="author" + UserProfile, blank=True, null=True, on_delete=models.PROTECT, related_name="author" ) - participants = ParentalManyToManyField(User, related_name="minutes") + participants = ParentalManyToManyField(UserProfile, related_name="minutes") labels = ClusterTaggableManager(through=TaggedMinutes, blank=True) body = CustomMarkdownField() guests = models.JSONField(blank=True, default=[]) @@ -186,9 +210,9 @@ class Minutes(BasePage): content_panels = Page.content_panels + [ FieldPanel("date"), - FieldPanel("moderator", widget=TomSelectWidget(label_field="username")), - FieldPanel("author", widget=TomSelectWidget(label_field="username")), - FieldPanel("participants", widget=TomSelectMultipleWidget(label_field="username")), + FieldPanel("moderator", widget=TomSelectWidget(label_field="display_name")), + FieldPanel("author", widget=TomSelectWidget(label_field="display_name")), + FieldPanel("participants", widget=TomSelectMultipleWidget(label_field="display_name")), FieldPanel("labels"), FieldPanel("body"), FieldPanel("guests"), From 49cabf40727c902f5390f31aa0dc1b2f9fff4288 Mon Sep 17 00:00:00 2001 From: frcroth Date: Tue, 21 Nov 2023 21:06:03 +0100 Subject: [PATCH 4/6] Fix sth --- .../0009_userprofile_for_minutes_display_name.py | 4 ++-- myhpi/core/models.py | 15 +++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/myhpi/core/migrations/0009_userprofile_for_minutes_display_name.py b/myhpi/core/migrations/0009_userprofile_for_minutes_display_name.py index d36fc735..5b673cc0 100644 --- a/myhpi/core/migrations/0009_userprofile_for_minutes_display_name.py +++ b/myhpi/core/migrations/0009_userprofile_for_minutes_display_name.py @@ -1,9 +1,9 @@ # Generated by Django 4.0.7 on 2023-11-21 19:05 -from django.conf import settings -from django.db import migrations, models import django.db.models.deletion import modelcluster.fields +from django.conf import settings +from django.db import migrations, models def create_user_profiles(apps, schema_editor): diff --git a/myhpi/core/models.py b/myhpi/core/models.py index 1da858c4..1d592743 100644 --- a/myhpi/core/models.py +++ b/myhpi/core/models.py @@ -1,13 +1,11 @@ from collections import defaultdict from datetime import date -from django.db.models import CharField, Value, F -from django.db.models.functions import Concat - from django import forms -from django.contrib.auth.models import Group, User +from django.contrib.auth.models import Group, User, UserManager from django.db import models -from django.db.models import BooleanField, CharField, DateField, ForeignKey, Model, Q +from django.db.models import BooleanField, CharField, DateField, F, ForeignKey, Model, Q, Value +from django.db.models.functions import Concat from django.http import HttpResponseRedirect from django_tomselect.widgets import TomSelectMultipleWidget, TomSelectWidget from modelcluster.contrib.taggit import ClusterTaggableManager @@ -43,7 +41,12 @@ def get_full_name(self): @receiver(post_save, sender=User) def post_user_save(sender, instance, created, **kwargs): display_name = instance.first_name + " " + instance.last_name - UserProfile.objects.find(user=instance).update_or_create(display_name=display_name) + user_profile = UserProfile.objects.get(user=instance.pk) + if user_profile: + user_profile.display_name = display_name + user_profile.save() + else: + UserProfile.objects.create(user=instance, display_name=display_name) class BasePage(Page): From afe4038c24859dfa7e29eaab70a7b9cb1419d892 Mon Sep 17 00:00:00 2001 From: frcroth Date: Tue, 21 Nov 2023 21:13:04 +0100 Subject: [PATCH 5/6] Fix tests --- myhpi/core/models.py | 10 +++++++--- myhpi/tests/core/setup.py | 31 ++++++++++++++++--------------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/myhpi/core/models.py b/myhpi/core/models.py index 1d592743..52a31cfb 100644 --- a/myhpi/core/models.py +++ b/myhpi/core/models.py @@ -33,6 +33,10 @@ def __str__(self): def get_full_name(self): return self.display_name + @staticmethod + def for_user(u): + return UserProfile.objects.get(user=u) + from django.db.models.signals import post_save from django.dispatch import receiver @@ -41,10 +45,10 @@ def get_full_name(self): @receiver(post_save, sender=User) def post_user_save(sender, instance, created, **kwargs): display_name = instance.first_name + " " + instance.last_name - user_profile = UserProfile.objects.get(user=instance.pk) + user_profile = UserProfile.objects.filter(user=instance.pk) if user_profile: - user_profile.display_name = display_name - user_profile.save() + user_profile[0].display_name = display_name + user_profile[0].save() else: UserProfile.objects.create(user=instance, display_name=display_name) diff --git a/myhpi/tests/core/setup.py b/myhpi/tests/core/setup.py index f13e71f0..724b0661 100644 --- a/myhpi/tests/core/setup.py +++ b/myhpi/tests/core/setup.py @@ -10,6 +10,7 @@ MinutesList, RootPage, SecondLevelMenuItem, + UserProfile, ) @@ -171,9 +172,9 @@ def setup_minutes(group, students_group, parent, user): date="2022-01-01", is_public=False, visible_for=[students_group, group], - moderator=user, - author=user, - participants=[user], + moderator=UserProfile.for_user(user), + author=UserProfile.for_user(user), + participants=[UserProfile.for_user(user)], body="These are the first minutes.", slug="first-minutes", ), @@ -182,9 +183,9 @@ def setup_minutes(group, students_group, parent, user): date="2022-02-02", is_public=False, visible_for=[students_group, group], - moderator=user, - author=user, - participants=[user], + moderator=UserProfile.for_user(user), + author=UserProfile.for_user(user), + participants=[UserProfile.for_user(user)], body="These are the second minutes.", slug="second-minutes", ), @@ -193,9 +194,9 @@ def setup_minutes(group, students_group, parent, user): date="2022-03-03", is_public=False, visible_for=[group], - moderator=user, - author=user, - participants=[user], + moderator=UserProfile.for_user(user), + author=UserProfile.for_user(user), + participants=[UserProfile.for_user(user)], body="These minutes are private.", slug="private-minutes", ), @@ -205,9 +206,9 @@ def setup_minutes(group, students_group, parent, user): is_public=False, live=False, visible_for=[group], - moderator=user, - author=user, - participants=[user], + moderator=UserProfile.for_user(user), + author=UserProfile.for_user(user), + participants=[UserProfile.for_user(user)], body="These minutes are unpublished.", slug="unpublished-minutes", ), @@ -216,9 +217,9 @@ def setup_minutes(group, students_group, parent, user): date="2022-05-05", is_public=False, visible_for=[students_group, group], - moderator=user, - author=user, - participants=[user], + moderator=UserProfile.for_user(user), + author=UserProfile.for_user(user), + participants=[UserProfile.for_user(user)], body="These minutes are the most recent.", slug="recent-minutes", ), From 5dec7e26e2469b64b6f5c0765b4833d2d3d790f0 Mon Sep 17 00:00:00 2001 From: frcroth Date: Tue, 21 Nov 2023 21:31:27 +0100 Subject: [PATCH 6/6] Remove markdown.util.etree --- myhpi/core/markdown/extensions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/myhpi/core/markdown/extensions.py b/myhpi/core/markdown/extensions.py index 0a662433..b7754c85 100644 --- a/myhpi/core/markdown/extensions.py +++ b/myhpi/core/markdown/extensions.py @@ -1,4 +1,5 @@ import re +import xml.etree.ElementTree from django.core.exceptions import ObjectDoesNotExist from django.utils.translation import gettext_lazy as _ @@ -145,7 +146,7 @@ def decrease(self, match): class InternalLinkPattern(LinkInlineProcessor): def handleMatch(self, m, data=None): - el = util.etree.Element("a") + el = xml.etree.ElementTree.Element("a") try: el.set("href", self.url(m.group("id"))) el.text = util.AtomicString(m.group("title"))