From 8b068efc826f9079b87c8b598214c325e9a321d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josef=20Kol=C3=A1=C5=99?= Date: Thu, 16 Nov 2023 02:45:36 +0100 Subject: [PATCH 1/7] feat(pickup-system): new requests-like plugin --- fiesta/apps/accounts/models/user.py | 1 + .../apps/buddy_system/models/configuration.py | 14 +- .../templates/buddy_system/editor/detail.html | 7 - .../buddy_system/editor/detail_form.html | 7 - .../buddy_system/editor/quick_match.html | 7 - .../buddy_system/editor/quick_match_form.html | 7 - .../buddy_system/new_buddy_request.html | 15 -- .../parts/new_buddy_request_form.html | 6 - fiesta/apps/buddy_system/urls.py | 4 +- fiesta/apps/buddy_system/views/editor.py | 175 +++-------------- fiesta/apps/buddy_system/views/matching.py | 92 ++------- fiesta/apps/buddy_system/views/request.py | 35 +--- .../dashboard/templates/dashboard/index.html | 11 +- .../fiestarequests/models/configuration.py | 13 -- fiesta/apps/fiestarequests/models/request.py | 1 + fiesta/apps/fiestarequests/tables/__init__.py | 0 fiesta/apps/fiestarequests/tables/editor.py | 105 +++++++++++ fiesta/apps/fiestarequests/views/__init__.py | 0 fiesta/apps/fiestarequests/views/editor.py | 71 +++++++ fiesta/apps/fiestarequests/views/matching.py | 41 ++++ fiesta/apps/fiestarequests/views/request.py | 38 ++++ fiesta/apps/pickup_system/__init__.py | 0 fiesta/apps/pickup_system/admin.py | 22 +++ fiesta/apps/pickup_system/apps.py | 58 ++++++ fiesta/apps/pickup_system/forms.py | 119 ++++++++++++ .../pickup_system/migrations/0001_initial.py | 83 +++++++++ .../apps/pickup_system/migrations/__init__.py | 0 fiesta/apps/pickup_system/models/__init__.py | 10 + .../pickup_system/models/configuration.py | 16 ++ fiesta/apps/pickup_system/models/files.py | 62 ++++++ fiesta/apps/pickup_system/models/request.py | 30 +++ .../pickup_system/dashboard_block.html | 128 +++++++++++++ .../pickup_system/index_international.html | 20 ++ .../templates/pickup_system/index_member.html | 17 ++ .../pickup_system/matching_requests.html | 140 ++++++++++++++ .../templates/pickup_system/my_pickups.html | 32 ++++ .../parts/pickup_request_note_help.html | 6 + .../parts/request_match_card.html | 176 ++++++++++++++++++ .../parts/requests_editor_match_btn.html | 11 ++ .../pickup_system/templatetags/__init__.py | 0 .../templatetags/pickup_system.py | 99 ++++++++++ fiesta/apps/pickup_system/urls.py | 28 +++ fiesta/apps/pickup_system/views/__init__.py | 5 + fiesta/apps/pickup_system/views/editor.py | 92 +++++++++ fiesta/apps/pickup_system/views/index.py | 48 +++++ fiesta/apps/pickup_system/views/matches.py | 17 ++ fiesta/apps/pickup_system/views/matching.py | 55 ++++++ fiesta/apps/pickup_system/views/request.py | 31 +++ .../migrations/0017_alter_plugin_app_label.py | 18 ++ fiesta/apps/sections/models/section.py | 1 + fiesta/apps/sections/views/membership.py | 56 +++++- fiesta/fiesta/settings/project.py | 1 + 52 files changed, 1696 insertions(+), 335 deletions(-) delete mode 100644 fiesta/apps/buddy_system/templates/buddy_system/editor/detail.html delete mode 100644 fiesta/apps/buddy_system/templates/buddy_system/editor/detail_form.html delete mode 100644 fiesta/apps/buddy_system/templates/buddy_system/editor/quick_match.html delete mode 100644 fiesta/apps/buddy_system/templates/buddy_system/editor/quick_match_form.html delete mode 100644 fiesta/apps/buddy_system/templates/buddy_system/new_buddy_request.html delete mode 100644 fiesta/apps/buddy_system/templates/buddy_system/parts/new_buddy_request_form.html create mode 100644 fiesta/apps/fiestarequests/tables/__init__.py create mode 100644 fiesta/apps/fiestarequests/tables/editor.py create mode 100644 fiesta/apps/fiestarequests/views/__init__.py create mode 100644 fiesta/apps/fiestarequests/views/editor.py create mode 100644 fiesta/apps/fiestarequests/views/matching.py create mode 100644 fiesta/apps/fiestarequests/views/request.py create mode 100644 fiesta/apps/pickup_system/__init__.py create mode 100644 fiesta/apps/pickup_system/admin.py create mode 100644 fiesta/apps/pickup_system/apps.py create mode 100644 fiesta/apps/pickup_system/forms.py create mode 100644 fiesta/apps/pickup_system/migrations/0001_initial.py create mode 100644 fiesta/apps/pickup_system/migrations/__init__.py create mode 100644 fiesta/apps/pickup_system/models/__init__.py create mode 100644 fiesta/apps/pickup_system/models/configuration.py create mode 100644 fiesta/apps/pickup_system/models/files.py create mode 100644 fiesta/apps/pickup_system/models/request.py create mode 100644 fiesta/apps/pickup_system/templates/pickup_system/dashboard_block.html create mode 100644 fiesta/apps/pickup_system/templates/pickup_system/index_international.html create mode 100644 fiesta/apps/pickup_system/templates/pickup_system/index_member.html create mode 100644 fiesta/apps/pickup_system/templates/pickup_system/matching_requests.html create mode 100644 fiesta/apps/pickup_system/templates/pickup_system/my_pickups.html create mode 100644 fiesta/apps/pickup_system/templates/pickup_system/parts/pickup_request_note_help.html create mode 100644 fiesta/apps/pickup_system/templates/pickup_system/parts/request_match_card.html create mode 100644 fiesta/apps/pickup_system/templates/pickup_system/parts/requests_editor_match_btn.html create mode 100644 fiesta/apps/pickup_system/templatetags/__init__.py create mode 100644 fiesta/apps/pickup_system/templatetags/pickup_system.py create mode 100644 fiesta/apps/pickup_system/urls.py create mode 100644 fiesta/apps/pickup_system/views/__init__.py create mode 100644 fiesta/apps/pickup_system/views/editor.py create mode 100644 fiesta/apps/pickup_system/views/index.py create mode 100644 fiesta/apps/pickup_system/views/matches.py create mode 100644 fiesta/apps/pickup_system/views/matching.py create mode 100644 fiesta/apps/pickup_system/views/request.py create mode 100644 fiesta/apps/plugins/migrations/0017_alter_plugin_app_label.py diff --git a/fiesta/apps/accounts/models/user.py b/fiesta/apps/accounts/models/user.py index 67a4bb61..64888295 100644 --- a/fiesta/apps/accounts/models/user.py +++ b/fiesta/apps/accounts/models/user.py @@ -51,6 +51,7 @@ class Meta(AbstractUser.Meta): # a few dynamic related models buddy_system_request_matches: models.QuerySet + pickup_system_request_matches: models.QuerySet profile: UserProfile @property diff --git a/fiesta/apps/buddy_system/models/configuration.py b/fiesta/apps/buddy_system/models/configuration.py index 37c436e1..8f97dc6f 100644 --- a/fiesta/apps/buddy_system/models/configuration.py +++ b/fiesta/apps/buddy_system/models/configuration.py @@ -1,12 +1,24 @@ from __future__ import annotations +from django.db import models from django.utils.translation import gettext_lazy as _ +from apps.fiestarequests.matching_policy import MatchingPoliciesRegister from apps.fiestarequests.models import BaseRequestSystemConfiguration class BuddySystemConfiguration(BaseRequestSystemConfiguration): - ... + matching_policy = models.CharField( + default=MatchingPoliciesRegister.DEFAULT_POLICY.id, + choices=MatchingPoliciesRegister.CHOICES, + max_length=32, + help_text=MatchingPoliciesRegister.DESCRIPTION, + ) + + @property + def matching_policy_instance(self): + # TODO: pass configuration? + return MatchingPoliciesRegister.get_policy_by_id(self.matching_policy) class Meta: verbose_name = _("buddy system configuration") diff --git a/fiesta/apps/buddy_system/templates/buddy_system/editor/detail.html b/fiesta/apps/buddy_system/templates/buddy_system/editor/detail.html deleted file mode 100644 index 3b27c10d..00000000 --- a/fiesta/apps/buddy_system/templates/buddy_system/editor/detail.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "fiesta/base-variants/center-card-2xl.html" %} - -{% load i18n %} - -{% block card_body %} - {% include "buddy_system/editor/detail_form.html" %} -{% endblock %} diff --git a/fiesta/apps/buddy_system/templates/buddy_system/editor/detail_form.html b/fiesta/apps/buddy_system/templates/buddy_system/editor/detail_form.html deleted file mode 100644 index 3edf3d4f..00000000 --- a/fiesta/apps/buddy_system/templates/buddy_system/editor/detail_form.html +++ /dev/null @@ -1,7 +0,0 @@ -
-

{{ object }}

- {% csrf_token %} - {{ form }} -
diff --git a/fiesta/apps/buddy_system/templates/buddy_system/editor/quick_match.html b/fiesta/apps/buddy_system/templates/buddy_system/editor/quick_match.html deleted file mode 100644 index 0c042f3f..00000000 --- a/fiesta/apps/buddy_system/templates/buddy_system/editor/quick_match.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "fiesta/base-variants/center-card-2xl.html" %} - -{% load i18n %} - -{% block card_body %} - {% include "buddy_system/editor/quick_match_form.html" %} -{% endblock %} diff --git a/fiesta/apps/buddy_system/templates/buddy_system/editor/quick_match_form.html b/fiesta/apps/buddy_system/templates/buddy_system/editor/quick_match_form.html deleted file mode 100644 index 0b7946bd..00000000 --- a/fiesta/apps/buddy_system/templates/buddy_system/editor/quick_match_form.html +++ /dev/null @@ -1,7 +0,0 @@ -
-

{{ object }}

- {% csrf_token %} - {{ form }} -
diff --git a/fiesta/apps/buddy_system/templates/buddy_system/new_buddy_request.html b/fiesta/apps/buddy_system/templates/buddy_system/new_buddy_request.html deleted file mode 100644 index 4a740576..00000000 --- a/fiesta/apps/buddy_system/templates/buddy_system/new_buddy_request.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "fiesta/base-variants/center-card-lg.html" %} - -{% load breadcrumbs %} -{% load i18n %} - -{% block upper_head %} - {% trans "New Request for Buddy" as title %} - {% breadcrumb_push_current_plugin %} - {% breadcrumb_push_item title %} -{% endblock upper_head %} - -{% block card_body %} -

Request for Buddy

- {% include "buddy_system/parts/new_buddy_request_form.html" %} -{% endblock %} diff --git a/fiesta/apps/buddy_system/templates/buddy_system/parts/new_buddy_request_form.html b/fiesta/apps/buddy_system/templates/buddy_system/parts/new_buddy_request_form.html deleted file mode 100644 index 1d9900c4..00000000 --- a/fiesta/apps/buddy_system/templates/buddy_system/parts/new_buddy_request_form.html +++ /dev/null @@ -1,6 +0,0 @@ -
- {% csrf_token %} - {{ form }} -
diff --git a/fiesta/apps/buddy_system/urls.py b/fiesta/apps/buddy_system/urls.py index 61202641..7442ece1 100644 --- a/fiesta/apps/buddy_system/urls.py +++ b/fiesta/apps/buddy_system/urls.py @@ -7,7 +7,7 @@ from .views.editor import BuddyRequestEditorDetailView, BuddyRequestsEditorView, QuickBuddyMatchView from .views.matches import MyBuddies from .views.matching import IssuerPictureServeView, MatcherPictureServeView, MatchingRequestsView, TakeBuddyRequestView -from .views.request import BuddySystemEntrance, NewRequestView, SignUpBeforeEntranceView, WannaBuddyView +from .views.request import BuddySystemEntrance, NewBuddyRequestView, SignUpBeforeEntranceView, WannaBuddyView urlpatterns = [ path("", BuddySystemIndexView.as_view(), name="index"), @@ -18,7 +18,7 @@ SignUpBeforeEntranceView.as_view(), name="sign-up-before-request", ), - path("new-request", NewRequestView.as_view(), name="new-request"), + path("new-request", NewBuddyRequestView.as_view(), name="new-request"), path("requests", BuddyRequestsEditorView.as_view(), name="requests"), path("my-buddies", MyBuddies.as_view(), name="my-buddies"), path("matching-requests", MatchingRequestsView.as_view(), name="matching-requests"), diff --git a/fiesta/apps/buddy_system/views/editor.py b/fiesta/apps/buddy_system/views/editor.py index 421dd850..e9cc5f80 100644 --- a/fiesta/apps/buddy_system/views/editor.py +++ b/fiesta/apps/buddy_system/views/editor.py @@ -1,127 +1,44 @@ from __future__ import annotations from django.contrib.messages.views import SuccessMessageMixin -from django.contrib.postgres.search import SearchVector -from django.db import transaction -from django.forms import TextInput -from django.urls import reverse_lazy +from django.urls import reverse, reverse_lazy from django.utils.translation import gettext_lazy as _ from django.views.generic import UpdateView -from django_filters import CharFilter, ChoiceFilter, ModelChoiceFilter -from django_tables2 import Column, TemplateColumn, tables +from django_tables2 import TemplateColumn +from django_tables2.columns.base import LinkTransform from django_tables2.utils import Accessor -from apps.accounts.models import User, UserProfile from apps.buddy_system.forms import BuddyRequestEditorForm, QuickBuddyMatchForm from apps.buddy_system.models import BuddyRequest, BuddyRequestMatch from apps.fiestaforms.views.htmx import HtmxFormMixin -from apps.fiestatables.columns import AvatarColumn, NaturalDatetimeColumn -from apps.fiestatables.filters import BaseFilterSet, ProperDateFromToRangeFilter +from apps.fiestarequests.tables.editor import BaseRequestsFilter, BaseRequestsTable +from apps.fiestarequests.views.editor import BaseQuickRequestMatchView from apps.fiestatables.views.tables import FiestaTableView from apps.sections.middleware.section_space import HttpRequest from apps.sections.views.mixins.membership import EnsurePrivilegedUserViewMixin -from apps.universities.models import Faculty from apps.utils.breadcrumbs import with_breadcrumb, with_object_breadcrumb from apps.utils.views import AjaxViewMixin -def related_faculties(request: HttpRequest): - return Faculty.objects.filter(university__section=request.in_space_of_section) - - -class RequestFilter(BaseFilterSet): - search = CharFilter( - method="filter_search", - label=_("Search"), - widget=TextInput(attrs={"placeholder": _("Hannah, Diego, Joe...")}), - ) - state = ChoiceFilter(choices=BuddyRequest.State.choices) - matched_when = ProperDateFromToRangeFilter( - field_name="match__created", - ) - - matcher_faculty = ModelChoiceFilter( - queryset=related_faculties, - label=_("Faculty of matcher"), - field_name="match__matcher__profile__faculty", - ) - - def filter_search(self, queryset, name, value): - return queryset.annotate( - search=SearchVector( - "issuer__last_name", - "issuer__first_name", - "match__matcher__last_name", - "match__matcher__first_name", - "state", - ) - ).filter(search=value) - - class Meta(BaseFilterSet.Meta): - pass - - -class BuddyRequestsTable(tables.Table): - issuer_name = Column( - accessor="issuer.full_name_official", - order_by=("issuer__last_name", "issuer__first_name", "issuer__username"), - attrs={"a": {"x-data": lambda: "modal($el.href)", "x-bind": "bind"}}, - linkify=("buddy_system:editor-detail", {"pk": Accessor("pk")}), - verbose_name=_("Issuer"), - ) - - issuer_picture = AvatarColumn(accessor="issuer.profile.picture", verbose_name="πŸ§‘") - - matcher_name = Column( - accessor="match.matcher.full_name_official", - order_by=( - "match__matcher__last_name", - "match__matcher__first_name", - "match__matcher__username", - ), - linkify=("sections:user-detail", {"pk": Accessor("match.matcher.pk")}), - ) - matcher_email = Column( - accessor="match.matcher.email", - visible=False, - ) - - matcher_picture = AvatarColumn( - accessor="match.matcher.profile.picture", - verbose_name=_("Matcher"), - ) - +class BuddyRequestsTable(BaseRequestsTable): match_request = TemplateColumn( template_name="buddy_system/parts/requests_editor_match_btn.html", exclude_from_export=True, order_by="match", ) - requested = NaturalDatetimeColumn(verbose_name=_("Requested"), accessor="created") - matched = NaturalDatetimeColumn( - accessor="match.created", - verbose_name=_("Matched"), - attrs={"td": {"title": None}}, # TODO: fix attrs accessor - ) + class Meta(BaseRequestsTable.Meta): + fields = BaseRequestsTable.Meta.fields + ("match_request",) - class Meta: - model = BuddyRequest - # TODO: dynamic by section preferences - fields = ("state",) - sequence = ( - "issuer_name", - "issuer_picture", - "state", - "matcher_name", - "matcher_picture", - "requested", - "matched", - "match_request", - "...", - ) - empty_text = _("No buddy requests found") + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) - attrs = dict(tbody={"hx-disable": True}) + if "issuer_name" in self.columns: + # sometimes excluded + self.columns["issuer_name"].link = LinkTransform( + attrs={"x-data": lambda: "modal($el.href)", "x-bind": "bind"}, + reverse_args=("buddy_system:editor-detail", {"pk": Accessor("pk")}), + ) @with_breadcrumb(_("Buddy System")) @@ -132,7 +49,7 @@ class BuddyRequestsEditorView( ): request: HttpRequest table_class = BuddyRequestsTable - filterset_class = RequestFilter + filterset_class = BaseRequestsFilter def get_queryset(self): return self.request.in_space_of_section.buddy_system_requests.select_related( @@ -149,66 +66,28 @@ class BuddyRequestEditorDetailView( AjaxViewMixin, UpdateView, ): - template_name = "buddy_system/editor/detail.html" - ajax_template_name = "buddy_system/editor/detail_form.html" + template_name = "fiestaforms/pages/card_page_for_ajax_form.html" + ajax_template_name = "fiestaforms/parts/ajax-form-container.html" model = BuddyRequest form_class = BuddyRequestEditorForm success_url = reverse_lazy("buddy_system:requests") success_message = _("Buddy request has been updated.") + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["form_url"] = reverse("pickup_system:editor-detail", kwargs={"pk": self.object.pk}) + return context + @with_breadcrumb(_("Quick Buddy Match")) @with_object_breadcrumb() -class QuickBuddyMatchView( - EnsurePrivilegedUserViewMixin, - SuccessMessageMixin, - HtmxFormMixin, - AjaxViewMixin, - UpdateView, -): - template_name = "buddy_system/editor/quick_match.html" - ajax_template_name = "buddy_system/editor/quick_match_form.html" +class QuickBuddyMatchView(BaseQuickRequestMatchView): model = BuddyRequest form_class = QuickBuddyMatchForm success_url = reverse_lazy("buddy_system:requests") success_message = _("Buddy request has been matched.") - def get_initial(self): - try: - matcher: User = self.get_object().match.matcher - profile: UserProfile = matcher.profile_or_none - return { - "matcher": matcher, - # SectionPluginsValidator ensures that faculty is required if BuddySystem is enabled - "matcher_faculty": profile.faculty if profile else None, - } - except BuddyRequestMatch.DoesNotExist: - return {} - - @transaction.atomic - def form_valid(self, form): - br: BuddyRequest = form.instance - - try: - if br.match: - # could be already matched by someone else - br.match.delete() - except BuddyRequestMatch.DoesNotExist: - pass - - matcher: User = form.cleaned_data.get("matcher") - - match = BuddyRequestMatch( - request=br, - matcher=matcher, - matcher_faculty=matcher.profile_or_none.faculty, - ) - - match.save() - - br.state = BuddyRequest.State.MATCHED - br.save(update_fields=["state"]) - - return super().form_valid(form) + form_url = "buddy_system:quick-match" + match_model = BuddyRequestMatch diff --git a/fiesta/apps/buddy_system/views/matching.py b/fiesta/apps/buddy_system/views/matching.py index 473d2d34..fd8c10f8 100644 --- a/fiesta/apps/buddy_system/views/matching.py +++ b/fiesta/apps/buddy_system/views/matching.py @@ -1,22 +1,15 @@ from __future__ import annotations -import uuid - -from django.contrib import messages from django.contrib.auth.mixins import PermissionRequiredMixin -from django.db import transaction -from django.utils.translation import gettext as _ from django.views.generic import ListView -from django.views.generic.detail import BaseDetailView -from django_htmx.http import HttpResponseClientRedirect from apps.buddy_system.models import BuddyRequest, BuddyRequestMatch, BuddySystemConfiguration -from apps.files.views import NamespacedFilesServeView -from apps.plugins.middleware.plugin import HttpRequest +from apps.fiestarequests.views.matching import BaseTakeRequestView +from apps.pickup_system.models.files import BaseIssuerPictureServeView, BaseMatcherPictureServeView +from apps.pickup_system.views.matching import ServeFilesFromPickupsMixin from apps.plugins.views import PluginConfigurationViewMixin from apps.sections.views.mixins.membership import EnsureLocalUserViewMixin from apps.sections.views.mixins.section_space import EnsureInSectionSpaceViewMixin -from apps.utils.models.query import Q class MatchingRequestsView( @@ -41,12 +34,12 @@ def get_queryset(self): class TakeBuddyRequestView( - EnsureInSectionSpaceViewMixin, - EnsureLocalUserViewMixin, - PermissionRequiredMixin, PluginConfigurationViewMixin[BuddySystemConfiguration], - BaseDetailView, + PermissionRequiredMixin, + BaseTakeRequestView, ): + match_model = BuddyRequestMatch + def has_permission(self): return self.configuration.matching_policy_instance.can_member_match @@ -56,74 +49,13 @@ def get_queryset(self): membership=self.request.membership, ) - @transaction.atomic - def post(self, request, pk: uuid.UUID): - br: BuddyRequest = self.get_object() - match = BuddyRequestMatch( - request=br, - matcher=self.request.user, - # not null since enabling buddy system requires a faculty - matcher_faculty=self.request.user.profile.faculty, - note=self.request.POST.get("note") or "", - ) - - # TODO: check matcher relation to responsible section - # TODO: reset any previous match for this BR - match.save() - - br.match = match - br.state = BuddyRequest.State.MATCHED - br.save(update_fields=["state"]) - - messages.success(request, _("Request successfully matched!")) - # TODO: target URL? - return HttpResponseClientRedirect("/") - - -class IssuerPictureServeView( - PluginConfigurationViewMixin[BuddySystemConfiguration], - NamespacedFilesServeView, -): - def has_permission(self, request: HttpRequest, name: str) -> bool: - # picture is from requests placed on my section - related_requests = request.membership.section.buddy_system_requests.filter( - issuer__profile__picture=name, - ) - - return ( - # does have the section enabled picture displaying? - (related_requests.exists() and self.configuration and self.configuration.display_issuer_picture) - # or are we in a matched request? - or ( - related_requests.filter( - state=BuddyRequest.State.MATCHED, - ) - .filter(match__matcher=request.user) - .exists() - ) - # or am I the issuer? - or (related_requests.filter(issuer=request.user).exists()) - ) +class IssuerPictureServeView(ServeFilesFromPickupsMixin, BaseIssuerPictureServeView): + ... class MatcherPictureServeView( - PluginConfigurationViewMixin[BuddySystemConfiguration], - NamespacedFilesServeView, + ServeFilesFromPickupsMixin, + BaseMatcherPictureServeView, ): - def has_permission(self, request: HttpRequest, name: str) -> bool: - # is the file in requests, for whose is the related section responsible? - related_requests = request.membership.section.buddy_system_requests.filter( - match__matcher__profile__picture=name, - ) - - # does have the section enabled picture displaying? - return ( - related_requests.filter( - state=BuddyRequest.State.MATCHED, - ) - .filter( - Q(match__matcher=request.user) | Q(issuer=request.user), - ) - .exists() - ) + ... diff --git a/fiesta/apps/buddy_system/views/request.py b/fiesta/apps/buddy_system/views/request.py index df283fef..4eeb7133 100644 --- a/fiesta/apps/buddy_system/views/request.py +++ b/fiesta/apps/buddy_system/views/request.py @@ -3,20 +3,19 @@ from allauth.account.views import SignupView from django.contrib import messages from django.contrib.auth import REDIRECT_FIELD_NAME -from django.contrib.messages.views import SuccessMessageMixin from django.db import transaction from django.http import HttpResponseRedirect from django.urls import reverse, reverse_lazy from django.utils.http import urlencode from django.utils.translation import gettext_lazy as _ -from django.views.generic import CreateView, TemplateView +from django.views.generic import TemplateView from apps.accounts.models import UserProfile from apps.buddy_system.forms import NewBuddyRequestForm from apps.buddy_system.models import BuddySystemConfiguration +from apps.fiestarequests.views.request import BaseNewRequestView from apps.plugins.views import PluginConfigurationViewMixin from apps.sections.models import SectionMembership, SectionsConfiguration -from apps.sections.views.mixins.membership import EnsureInternationalUserViewMixin from apps.sections.views.mixins.section_space import EnsureInSectionSpaceViewMixin @@ -93,39 +92,15 @@ def form_valid(self, form): return response -class NewRequestView( - EnsureInSectionSpaceViewMixin, - EnsureInternationalUserViewMixin, - SuccessMessageMixin, - CreateView, -): - template_name = "buddy_system/new_buddy_request.html" +class NewBuddyRequestView(BaseNewRequestView): form_class = NewBuddyRequestForm success_message = _("Your buddy request has been successfully created!") success_url = reverse_lazy("buddy_system:index") def get_initial(self): + i = super().get_initial() p: UserProfile = self.request.user.profile_or_none - return { - "responsible_section": self.request.in_space_of_section, - "issuer": self.request.user, - "issuer_faculty": p.faculty if p else None, + return i | { "interests": p.interests if p else None, } - - def form_valid(self, form): - # override to be sure - form.instance.responsible_section = self.request.in_space_of_section - form.instance.issuer = self.request.user - - profile: UserProfile = self.request.user.profile_or_none - if not profile.faculty: - profile.faculty = form.instance.issuer_faculty - profile.save(update_fields=("faculty",)) - - if not profile.university: - profile.university = form.instance.issuer_faculty.university - profile.save(update_fields=("university",)) - - return super().form_valid(form) diff --git a/fiesta/apps/dashboard/templates/dashboard/index.html b/fiesta/apps/dashboard/templates/dashboard/index.html index 8e7cb9ff..a856e7d3 100644 --- a/fiesta/apps/dashboard/templates/dashboard/index.html +++ b/fiesta/apps/dashboard/templates/dashboard/index.html @@ -78,8 +78,15 @@ {% if blocks.accounts %}
{% include blocks.accounts with request=request %}
{% endif %} - {% if blocks.esncards %} -
{% include blocks.esncards with request=request %}
+ {% if blocks.esncards or blocks.pickup_system %} +
+ {% if blocks.esncards %} + {% include blocks.esncards with request=request %} + {% endif %} + {% if blocks.pickup_system %} + {% include blocks.pickup_system with request=request %} + {% endif %} +
{% endif %} diff --git a/fiesta/apps/fiestarequests/models/configuration.py b/fiesta/apps/fiestarequests/models/configuration.py index 72842fa4..20e1eadd 100644 --- a/fiesta/apps/fiestarequests/models/configuration.py +++ b/fiesta/apps/fiestarequests/models/configuration.py @@ -3,7 +3,6 @@ from django.db import models from django.utils.translation import gettext_lazy as _ -from apps.fiestarequests.matching_policy import MatchingPoliciesRegister from apps.plugins.models import BasePluginConfiguration @@ -17,22 +16,10 @@ class BaseRequestSystemConfiguration(BasePluginConfiguration): rolling_limit = models.PositiveSmallIntegerField(default=0) - matching_policy = models.CharField( - default=MatchingPoliciesRegister.DEFAULT_POLICY.id, - choices=MatchingPoliciesRegister.CHOICES, - max_length=32, - help_text=MatchingPoliciesRegister.DESCRIPTION, - ) - enable_note_from_matcher = models.BooleanField( default=True, help_text=_("Allows matcher to reply with custom notes to the request issuer"), ) - @property - def matching_policy_instance(self): - # TODO: pass configuration? - return MatchingPoliciesRegister.get_policy_by_id(self.matching_policy) - class Meta: abstract = True diff --git a/fiesta/apps/fiestarequests/models/request.py b/fiesta/apps/fiestarequests/models/request.py index e46f94b3..0878b85f 100644 --- a/fiesta/apps/fiestarequests/models/request.py +++ b/fiesta/apps/fiestarequests/models/request.py @@ -24,6 +24,7 @@ class State(TextChoices): issuer: models.ForeignKey | User responsible_section: models.ForeignKey | Section note: models.TextField | str + match: BaseRequestMatchProtocol | models.Model objects: BaseRequestManager diff --git a/fiesta/apps/fiestarequests/tables/__init__.py b/fiesta/apps/fiestarequests/tables/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fiesta/apps/fiestarequests/tables/editor.py b/fiesta/apps/fiestarequests/tables/editor.py new file mode 100644 index 00000000..bde29574 --- /dev/null +++ b/fiesta/apps/fiestarequests/tables/editor.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +from django.contrib.postgres.search import SearchVector +from django.forms import TextInput +from django.utils.translation import gettext_lazy as _ +from django_filters import CharFilter, ChoiceFilter, ModelChoiceFilter +from django_tables2 import Column, tables +from django_tables2.utils import Accessor + +from apps.buddy_system.models import BuddyRequest +from apps.fiestarequests.models.request import BaseRequestProtocol +from apps.fiestatables.columns import AvatarColumn, NaturalDatetimeColumn +from apps.fiestatables.filters import BaseFilterSet, ProperDateFromToRangeFilter +from apps.plugins.middleware.plugin import HttpRequest +from apps.universities.models import Faculty + + +def related_faculties(request: HttpRequest): + return Faculty.objects.filter(university__section=request.in_space_of_section) + + +class BaseRequestsFilter(BaseFilterSet): + search = CharFilter( + method="filter_search", + label=_("Search"), + widget=TextInput(attrs={"placeholder": _("Hannah, Diego, Joe...")}), + ) + state = ChoiceFilter(choices=BaseRequestProtocol.State.choices) + matched_when = ProperDateFromToRangeFilter( + field_name="match__created", + ) + + matcher_faculty = ModelChoiceFilter( + queryset=related_faculties, + label=_("Faculty of matcher"), + field_name="match__matcher__profile__faculty", + ) + + def filter_search(self, queryset, name, value): + return queryset.annotate( + search=SearchVector( + "issuer__last_name", + "issuer__first_name", + "match__matcher__last_name", + "match__matcher__first_name", + "state", + ) + ).filter(search=value) + + class Meta(BaseFilterSet.Meta): + pass + + +class BaseRequestsTable(tables.Table): + issuer_name = Column( + accessor="issuer.full_name_official", + order_by=("issuer__last_name", "issuer__first_name", "issuer__username"), + verbose_name=_("Issuer"), + ) + + issuer_picture = AvatarColumn(accessor="issuer.profile.picture", verbose_name="πŸ§‘") + + matcher_name = Column( + accessor="match.matcher.full_name_official", + order_by=( + "match__matcher__last_name", + "match__matcher__first_name", + "match__matcher__username", + ), + linkify=("sections:user-detail", {"pk": Accessor("match.matcher.pk")}), + ) + matcher_email = Column( + accessor="match.matcher.email", + visible=False, + ) + + matcher_picture = AvatarColumn( + accessor="match.matcher.profile.picture", + verbose_name=_("Matcher"), + ) + + requested = NaturalDatetimeColumn(verbose_name=_("Requested"), accessor="created") + matched = NaturalDatetimeColumn( + accessor="match.created", + verbose_name=_("Matched"), + attrs={"td": {"title": None}}, # TODO: fix attrs accessor + ) + + class Meta: + model = BuddyRequest + # TODO: dynamic by section preferences + fields = ("state",) + sequence = ( + "issuer_name", + "issuer_picture", + "state", + "matcher_name", + "matcher_picture", + "requested", + "matched", + "...", + ) + empty_text = _("No requests found") + + attrs = dict(tbody={"hx-disable": True}) diff --git a/fiesta/apps/fiestarequests/views/__init__.py b/fiesta/apps/fiestarequests/views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fiesta/apps/fiestarequests/views/editor.py b/fiesta/apps/fiestarequests/views/editor.py new file mode 100644 index 00000000..840edbb4 --- /dev/null +++ b/fiesta/apps/fiestarequests/views/editor.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +from django.contrib.messages.views import SuccessMessageMixin +from django.core.exceptions import ObjectDoesNotExist +from django.db import models, transaction +from django.urls import reverse +from django.views.generic import UpdateView + +from apps.accounts.models import User, UserProfile +from apps.fiestaforms.views.htmx import HtmxFormMixin +from apps.fiestarequests.models.request import BaseRequestProtocol +from apps.sections.views.mixins.membership import EnsurePrivilegedUserViewMixin +from apps.utils.views import AjaxViewMixin + + +class BaseQuickRequestMatchView( + EnsurePrivilegedUserViewMixin, + SuccessMessageMixin, + HtmxFormMixin, + AjaxViewMixin, + UpdateView, +): + template_name = "fiestaforms/pages/card_page_for_ajax_form.html" + ajax_template_name = "fiestaforms/parts/ajax-form-container.html" + + # to be set by subclasses + form_url: str + match_model: type[models.Model] + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["form_url"] = reverse(self.form_url, kwargs={"pk": self.object.pk}) + return context + + def get_initial(self): + try: + matcher: User = self.get_object().match.matcher + profile: UserProfile = matcher.profile_or_none + return { + "matcher": matcher, + # SectionPluginsValidator ensures that faculty is required if PickupSystem is enabled + "matcher_faculty": profile.faculty if profile else None, + } + except ObjectDoesNotExist: + return {} + + @transaction.atomic + def form_valid(self, form): + br: BaseRequestProtocol | models.Model = form.instance + + try: + if br.match: + # could be already matched by someone else + br.match.delete() + except ObjectDoesNotExist: + pass + + matcher: User = form.cleaned_data.get("matcher") + + match = self.match_model( + request=br, + matcher=matcher, + matcher_faculty=matcher.profile_or_none.faculty, + ) + + match.save() + + br.state = BaseRequestProtocol.State.MATCHED + br.save(update_fields=["state"]) + + return super().form_valid(form) diff --git a/fiesta/apps/fiestarequests/views/matching.py b/fiesta/apps/fiestarequests/views/matching.py new file mode 100644 index 00000000..6a44e732 --- /dev/null +++ b/fiesta/apps/fiestarequests/views/matching.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import uuid + +from django.contrib import messages +from django.db import models, transaction +from django.utils.translation import gettext as _ +from django.views.generic.detail import BaseDetailView +from django_htmx.http import HttpResponseClientRedirect + +from apps.fiestarequests.models.request import BaseRequestMatchProtocol, BaseRequestProtocol +from apps.sections.views.mixins.membership import EnsureLocalUserViewMixin +from apps.sections.views.mixins.section_space import EnsureInSectionSpaceViewMixin + + +class BaseTakeRequestView(EnsureInSectionSpaceViewMixin, EnsureLocalUserViewMixin, BaseDetailView): + match_model: type[BaseRequestMatchProtocol] | type[models.Model] + + @transaction.atomic + def post(self, request, pk: uuid.UUID): + br: BaseRequestProtocol = self.get_object() + + match = self.match_model( + request=br, + matcher=self.request.user, + # not null since enabling pickup system requires a faculty + matcher_faculty=self.request.user.profile.faculty, + note=self.request.POST.get("note") or "", + ) + + # TODO: check matcher relation to responsible section + # TODO: reset any previous match for this BR + match.save() + + br.match = match + br.state = BaseRequestProtocol.State.MATCHED + br.save(update_fields=["state"]) + + messages.success(request, _("Request successfully matched!")) + # TODO: target URL? + return HttpResponseClientRedirect("/") diff --git a/fiesta/apps/fiestarequests/views/request.py b/fiesta/apps/fiestarequests/views/request.py new file mode 100644 index 00000000..db7c35e2 --- /dev/null +++ b/fiesta/apps/fiestarequests/views/request.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +from django.contrib.messages.views import SuccessMessageMixin +from django.views.generic import CreateView + +from apps.accounts.models import UserProfile +from apps.fiestaforms.views.htmx import HtmxFormMixin +from apps.sections.views.mixins.membership import EnsureInternationalUserViewMixin +from apps.sections.views.mixins.section_space import EnsureInSectionSpaceViewMixin +from apps.utils.views import AjaxViewMixin + + +class BaseNewRequestView( + EnsureInSectionSpaceViewMixin, + EnsureInternationalUserViewMixin, + SuccessMessageMixin, + AjaxViewMixin, + HtmxFormMixin, + CreateView, +): + template_name = "fiestaforms/pages/card_page_for_ajax_form.html" + ajax_template_name = "fiestaforms/parts/ajax-form-container.html" + + def get_initial(self): + p: UserProfile = self.request.user.profile_or_none + return { + "responsible_section": self.request.in_space_of_section, + "issuer": self.request.user, + "issuer_faculty": p.faculty if p else None, + # "interests": p.interests if p else None, + } + + def form_valid(self, form): + # override to be sure + form.instance.responsible_section = self.request.in_space_of_section + form.instance.issuer = self.request.user + + return super().form_valid(form) diff --git a/fiesta/apps/pickup_system/__init__.py b/fiesta/apps/pickup_system/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fiesta/apps/pickup_system/admin.py b/fiesta/apps/pickup_system/admin.py new file mode 100644 index 00000000..de49f7c4 --- /dev/null +++ b/fiesta/apps/pickup_system/admin.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from django.contrib import admin + +from ..fiestarequests.admin import BaseRequestAdmin, BaseRequestMatchAdmin +from ..plugins.admin import BaseChildConfigurationAdmin +from .models import PickupRequest, PickupRequestMatch, PickupSystemConfiguration + + +@admin.register(PickupSystemConfiguration) +class PickupSystemConfigurationAdmin(BaseChildConfigurationAdmin): + show_in_index = True + + +@admin.register(PickupRequest) +class PickupRequestAdmin(BaseRequestAdmin): + pass + + +@admin.register(PickupRequestMatch) +class PickupRequestMatchAdmin(BaseRequestMatchAdmin): + pass diff --git a/fiesta/apps/pickup_system/apps.py b/fiesta/apps/pickup_system/apps.py new file mode 100644 index 00000000..bffb13fc --- /dev/null +++ b/fiesta/apps/pickup_system/apps.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import typing + +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from apps.plugins.plugin import BasePluginAppConfig +from apps.utils.templatetags.navigation import NavigationItemSpec + +if typing.TYPE_CHECKING: + from apps.plugins.middleware.plugin import HttpRequest + from apps.plugins.models import Plugin + + +class PickupSystemConfig(BasePluginAppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.pickup_system" + verbose_name = _("Pickup System") + emoji = "🀼" + description = _("Tools for managing pickup of your students.") + + configuration_model = "pickup_system.PickupSystemConfiguration" + + login_not_required_urls = ( + "wanna-pickup", + "sign-up-before-request", + ) + + membership_not_required_urls = ("new-request",) + + def as_navigation_item(self, request: HttpRequest, bound_plugin: Plugin) -> NavigationItemSpec | None: + base = ( + super() + .as_navigation_item(request, bound_plugin) + ._replace( + children=( + [ + NavigationItemSpec(title=_("My Pickups"), url=reverse("pickup_system:my-pickups")), + ] + if request.membership.is_local + else [] + ), + ) + ) + + if not request.membership.is_privileged: + return base + + return base._replace( + children=base.children + + [ + NavigationItemSpec(title=_("Requests"), url=reverse("pickup_system:requests")), + ], + ) + + +__all__ = ["PickupSystemConfig"] diff --git a/fiesta/apps/pickup_system/forms.py b/fiesta/apps/pickup_system/forms.py new file mode 100644 index 00000000..adf80931 --- /dev/null +++ b/fiesta/apps/pickup_system/forms.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +from django.core.exceptions import ValidationError +from django.forms import BooleanField, HiddenInput, fields_for_model +from django.template.loader import render_to_string +from django.utils.functional import lazy +from django.utils.translation import gettext_lazy as _ + +from apps.accounts.models import User, UserProfile +from apps.fiestaforms.forms import BaseModelForm +from apps.fiestaforms.widgets.models import ActiveLocalMembersFromSectionWidget, FacultyWidget, UserWidget +from apps.pickup_system.models import PickupRequest, PickupRequestMatch + +USER_PROFILE_CONTACT_FIELDS = fields_for_model( + UserProfile, + fields=("facebook", "instagram", "telegram", "whatsapp"), +) + + +class NewPickupRequestForm(BaseModelForm): + submit_text = _("Send request for pickup") + + # TODO: group field somehow and add group headings + facebook, instagram, telegram, whatsapp = USER_PROFILE_CONTACT_FIELDS.values() + + approving_request = BooleanField(required=True, label=_("I really want a pickup")) + + class Meta: + model = PickupRequest + fields = ( + "note", + # "interests", + "issuer", + "issuer_faculty", + ) + field_classes = { + # "interests": ChoicedArrayField, + } + widgets = { + "issuer": HiddenInput, + "issuer_faculty": FacultyWidget, + } + labels = { + "note": _("Tell us about yourself"), + # "interests": _("What are you into?"), + "issuer_faculty": _("Your faculty"), + } + help_texts = { + "note": lazy( + lambda: render_to_string("pickup_system/parts/pickup_request_note_help.html"), + str, + ) + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if self.initial.get("issuer_faculty"): + self.fields["issuer_faculty"].disabled = True + + +# TODO: add save/load of contacts to/from user_profile + + +class PickupRequestEditorForm(BaseModelForm): + submit_text = _("Save") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.fields["issuer"].disabled = True + + if self.instance.state != PickupRequest.State.CREATED: + # self.fields["matched_by"].disabled = True + # self.fields["matched_at"].disabled = True + self.fields["note"].disabled = True + # self.fields["interests"].disabled = True + + class Meta: + model = PickupRequest + fields = ( + "issuer", + "state", + "note", + # "interests", + # "matched_by", + # "matched_at", + ) + field_classes = { + # "interests": ChoicedArrayField, + # "matched_at": DateTimeLocalField, + } + widgets = { + "issuer": UserWidget, + # "matched_by": ActiveLocalMembersFromSectionWidget, + } + + +class QuickPickupMatchForm(BaseModelForm): + submit_text = _("Match") + instance: PickupRequestMatch + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + class Meta: + model = PickupRequestMatch + fields = ("matcher",) + widgets = { + "matcher": ActiveLocalMembersFromSectionWidget, + } + + def clean_matcher(self): + matcher: User = self.cleaned_data["matcher"] + + if not matcher.profile_or_none.faculty: + raise ValidationError(_("This user has not set their faculty. Please ask them to do so or do it yourself.")) + + return matcher diff --git a/fiesta/apps/pickup_system/migrations/0001_initial.py b/fiesta/apps/pickup_system/migrations/0001_initial.py new file mode 100644 index 00000000..30e828c8 --- /dev/null +++ b/fiesta/apps/pickup_system/migrations/0001_initial.py @@ -0,0 +1,83 @@ +# Generated by Django 4.2.7 on 2023-11-15 23:51 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import django_extensions.db.fields +import django_lifecycle.mixins +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('sections', '0021_alter_sectionuniversity_section_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('universities', '0003_alter_faculty_created_alter_faculty_university_and_more'), + ('plugins', '0017_alter_plugin_app_label'), + ] + + operations = [ + migrations.CreateModel( + name='PickupRequest', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='created')), + ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')), + ('state', models.CharField(choices=[('created', 'Created'), ('matched', 'Matched'), ('cancelled', 'Cancelled')], default='created', max_length=16, verbose_name='state')), + ('note', models.TextField(verbose_name='text from issuer')), + ('issuer', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='pickup_system_issued_requests', to=settings.AUTH_USER_MODEL, verbose_name='issuer')), + ('issuer_faculty', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='pickup_system_issued_requests', to='universities.faculty', verbose_name="issuer's faculty")), + ('responsible_section', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='pickup_system_requests', to='sections.section', verbose_name='responsible section')), + ], + options={ + 'verbose_name': 'pickup request', + 'verbose_name_plural': 'pickup requests', + 'ordering': ('-created',), + 'get_latest_by': 'modified', + 'abstract': False, + }, + bases=(django_lifecycle.mixins.LifecycleModelMixin, models.Model), + ), + migrations.CreateModel( + name='PickupSystemConfiguration', + fields=[ + ('basepluginconfiguration_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='plugins.basepluginconfiguration')), + ('display_issuer_picture', models.BooleanField(default=False)), + ('display_issuer_gender', models.BooleanField(default=False)), + ('display_issuer_country', models.BooleanField(default=False)), + ('display_issuer_university', models.BooleanField(default=False)), + ('display_issuer_faculty', models.BooleanField(default=False)), + ('display_request_creation_date', models.BooleanField(default=True)), + ('rolling_limit', models.PositiveSmallIntegerField(default=0)), + ('enable_note_from_matcher', models.BooleanField(default=True, help_text='Allows matcher to reply with custom notes to the request issuer')), + ], + options={ + 'verbose_name': 'pickup system configuration', + 'verbose_name_plural': 'pickup system configurations', + }, + bases=('plugins.basepluginconfiguration',), + ), + migrations.CreateModel( + name='PickupRequestMatch', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='created')), + ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')), + ('note', models.TextField(blank=True, verbose_name='text from matcher')), + ('matcher', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='pickup_system_request_matches', to=settings.AUTH_USER_MODEL, verbose_name='matched by')), + ('matcher_faculty', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, related_name='pickup_system_request_matches', to='universities.faculty', verbose_name="matcher's faculty")), + ('request', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='match', to='pickup_system.pickuprequest', verbose_name='request')), + ], + options={ + 'verbose_name': 'pickup request match', + 'verbose_name_plural': 'pickup request matches', + 'ordering': ('-created',), + 'get_latest_by': 'modified', + 'abstract': False, + }, + ), + ] diff --git a/fiesta/apps/pickup_system/migrations/__init__.py b/fiesta/apps/pickup_system/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fiesta/apps/pickup_system/models/__init__.py b/fiesta/apps/pickup_system/models/__init__.py new file mode 100644 index 00000000..4a8db16a --- /dev/null +++ b/fiesta/apps/pickup_system/models/__init__.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from .configuration import PickupSystemConfiguration +from .request import PickupRequest, PickupRequestMatch + +__all__ = [ + "PickupSystemConfiguration", + "PickupRequest", + "PickupRequestMatch", +] diff --git a/fiesta/apps/pickup_system/models/configuration.py b/fiesta/apps/pickup_system/models/configuration.py new file mode 100644 index 00000000..aed03de3 --- /dev/null +++ b/fiesta/apps/pickup_system/models/configuration.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from django.utils.translation import gettext_lazy as _ + +from apps.fiestarequests.models import BaseRequestSystemConfiguration + + +class PickupSystemConfiguration(BaseRequestSystemConfiguration): + ... + + class Meta: + verbose_name = _("pickup system configuration") + verbose_name_plural = _("pickup system configurations") + + +__all__ = ["PickupSystemConfiguration"] diff --git a/fiesta/apps/pickup_system/models/files.py b/fiesta/apps/pickup_system/models/files.py new file mode 100644 index 00000000..1fe588c0 --- /dev/null +++ b/fiesta/apps/pickup_system/models/files.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from apps.fiestarequests.models import BaseRequestSystemConfiguration +from apps.fiestarequests.models.request import BaseRequestProtocol +from apps.files.views import NamespacedFilesServeView +from apps.plugins.middleware.plugin import HttpRequest +from apps.plugins.views import PluginConfigurationViewMixin +from apps.utils.models.query import Q + + +class BaseIssuerPictureServeView( + PluginConfigurationViewMixin[BaseRequestSystemConfiguration], + NamespacedFilesServeView, +): + def get_request_queryset(self, request: HttpRequest): + raise NotImplementedError + + def has_permission(self, request: HttpRequest, name: str) -> bool: + # picture is from requests placed on my section + related_requests = self.get_request_queryset(request).filter( + issuer__profile__picture=name, + ) + + return ( + # does have the section enabled picture displaying? + (related_requests.exists() and self.configuration and self.configuration.display_issuer_picture) + # or are we in a matched request? + or ( + related_requests.filter( + state=BaseRequestProtocol.State.MATCHED, + ) + .filter(match__matcher=request.user) + .exists() + ) + # or am I the issuer? + or (related_requests.filter(issuer=request.user).exists()) + ) + + +class BaseMatcherPictureServeView( + PluginConfigurationViewMixin[BaseRequestSystemConfiguration], + NamespacedFilesServeView, +): + def get_request_queryset(self, request: HttpRequest): + raise NotImplementedError + + def has_permission(self, request: HttpRequest, name: str) -> bool: + # is the file in requests, for whose is the related section responsible? + related_requests = self.get_request_queryset(request).filter( + match__matcher__profile__picture=name, + ) + + # does have the section enabled picture displaying? + return ( + related_requests.filter( + state=BaseRequestProtocol.State.MATCHED, + ) + .filter( + Q(match__matcher=request.user) | Q(issuer=request.user), + ) + .exists() + ) diff --git a/fiesta/apps/pickup_system/models/request.py b/fiesta/apps/pickup_system/models/request.py new file mode 100644 index 00000000..2c9b172c --- /dev/null +++ b/fiesta/apps/pickup_system/models/request.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from django.utils.translation import gettext_lazy as _ + +from apps.fiestarequests.models import base_request_model_factory + +BaseRequestForPickupSystem, BaseRequestMatchForPickupSystem = base_request_model_factory( + final_request_model_name="pickup_system.PickupRequest", + related_base="pickup_system", +) + + +class PickupRequest(BaseRequestForPickupSystem): + # TODO: date/time/place + + class Meta(BaseRequestForPickupSystem.Meta): + verbose_name = _("pickup request") + verbose_name_plural = _("pickup requests") + + def __str__(self): + return f"Pickup Request {self.issuer}: {self.get_state_display()}" + + +class PickupRequestMatch(BaseRequestMatchForPickupSystem): + class Meta(BaseRequestForPickupSystem.Meta): + verbose_name = _("pickup request match") + verbose_name_plural = _("pickup request matches") + + def __str__(self): + return f"Pickup Request Match {self.matcher}: {self.request}" diff --git a/fiesta/apps/pickup_system/templates/pickup_system/dashboard_block.html b/fiesta/apps/pickup_system/templates/pickup_system/dashboard_block.html new file mode 100644 index 00000000..64b1f2ed --- /dev/null +++ b/fiesta/apps/pickup_system/templates/pickup_system/dashboard_block.html @@ -0,0 +1,128 @@ +{% load pickup_system %} +{% load utils %} +{% load i18n %} +{% load user_profile %} + +{% get_pickup_system_configuration as configuration %} + +
+
+
Pickup System
+ {% if request.membership.is_international %} + {% get_current_pickup_request_of_user as br %} + {% if not br %} +
βœ–οΈ Not requested yet
+ + {% elif br.state == br.State.CREATED %} +
βŒ› Waiting for match
+ {% get_waiting_pickup_requests_placed_before br as waiting_total %} +
+ There is {{ waiting_total }} waiting request{{ waiting_total|pluralize:"s" }} before yours. +
+ {% elif br.state == br.State.MATCHED %} + {% get_user_picture br.match.matcher as pickup_picture %} +
+ {% trans "find out about your pickup" %} + +
+
+ {% if pickup_picture %} + Matched pickup picture + {% else %} + {{ br.match.matcher.first_name|first }}{{ br.match.matcher.last_name|first }} + {% endif %} +
+
+ +
+ +
βœ… Matched
+
+ It's a match! +
+ You have been matched with {{ br.match.matcher.full_name }}. +
+ {% endif %} + {% else %} + {% get_waiting_requests_to_match as waiting_brs %} +
+ + {% with waiting_brs.count as count %} + {{ count }} + waiting request{{ count|pluralize:"s" }} + {% endwith %} + + {% if waiting_brs.exists %} + show + {% endif %} +
+ + {% endif %} +
+ + {% if request.membership.is_local %} +
+
My Pickups
+ + {% get_matched_pickup_requests as request_matches %} + {% if not request_matches.exists %} +
So empty here πŸ˜”
+ + {% if configuration.matching_policy_instance.can_member_match %} + + {% else %} +
Pickup system team is looking for the best match for you.
+ {% endif %} + {% else %} +
+
+ {% for rm in request_matches|slice:":3" %} + {% get_user_picture rm.request.issuer as pickup_picture %} + + {% if pickup_picture %} + +
+ {{ rm.request.issuer }} +
+
+ {% else %} + + + {{ rm.request.issuer.first_name|first }}{{ rm.request.issuer.last_name|first }} + + + {% endif %} + {% endfor %} + {% if request_matches|length > 3 %} + + {% endif %} +
+
+ {% endif %} +
+ {% endif %} +
diff --git a/fiesta/apps/pickup_system/templates/pickup_system/index_international.html b/fiesta/apps/pickup_system/templates/pickup_system/index_international.html new file mode 100644 index 00000000..b867f5ba --- /dev/null +++ b/fiesta/apps/pickup_system/templates/pickup_system/index_international.html @@ -0,0 +1,20 @@ +{% extends "fiesta/base.html" %} +{% load pickup_system %} +{% load user_profile %} +{% load utils %} + +{% load i18n %} +{% load breadcrumbs %} + +{% block upper_head %} + {% trans "My Requests" as title %} + {% breadcrumb_push_item title %} +{% endblock upper_head %} + +{% block main %} + {# international without any requests are redirected to /wanna-pickup by view #} + {% for br in requests %} + {% blocktranslate with created=br.created|date asvar title %}Your Request from {{ created }}{% endblocktranslate %} + {% include "pickup_system/parts/request_match_card.html" with br=br title=title connect_with=br.match.matcher %} + {% endfor %} +{% endblock %} diff --git a/fiesta/apps/pickup_system/templates/pickup_system/index_member.html b/fiesta/apps/pickup_system/templates/pickup_system/index_member.html new file mode 100644 index 00000000..6b698723 --- /dev/null +++ b/fiesta/apps/pickup_system/templates/pickup_system/index_member.html @@ -0,0 +1,17 @@ +{% extends "fiesta/base.html" %} + +{% load static %} +{% load i18n %} +{% load breadcrumbs %} + +{% block main %} +
+
πŸ“
+
+

{% trans "Pickup System" %}

+

Ready to help students?

+ {% trans "Show pickup requests" %} +
+
+{% endblock %} diff --git a/fiesta/apps/pickup_system/templates/pickup_system/matching_requests.html b/fiesta/apps/pickup_system/templates/pickup_system/matching_requests.html new file mode 100644 index 00000000..e885947b --- /dev/null +++ b/fiesta/apps/pickup_system/templates/pickup_system/matching_requests.html @@ -0,0 +1,140 @@ +{% extends "fiesta/base.html" %} + +{% load pickup_system %} +{% load breadcrumbs %} +{% load i18n %} + +{% block upper_head %} + {% trans "Waiting requests" as title %} + {% breadcrumb_push_current_plugin %} + {% breadcrumb_push_item title %} +{% endblock upper_head %} + +{% block main %} +
+ {% for br in object_list %} +
+
+ {% if configuration.display_issuer_picture and br.issuer.profile.picture %} + {# TODO: img size #} + {% trans + {% else %} + + + + + {% endif %} +
+
+ + {% if configuration.display_issuer_gender %} + + + + + {% endif %} + {% if configuration.display_issuer_faculty %} + + + + + {% endif %} + {% if configuration.display_issuer_country %} + + + + + {% endif %} + {% if configuration.display_issuer_university %} + + + + + {% endif %} +
{% trans "gender" %}{{ br.issuer.profile.get_gender_display }}
{% blocktrans %}faculty on exchange{% endblocktrans %}{{ br.issuer.profile.faculty }}
{% trans "country" %}{{ br.issuer.profile.nationality.unicode_flag }} {{ br.issuer.profile.nationality.name }}
{% blocktrans %}home university{% endblocktrans %}{{ br.issuer.profile.home_university }}
+ {#

are added by filter #} + {{ br.note|censor_description|linebreaks }} +


+
+ {% if configuration.display_request_creation_date %} + {{ br.created|date:'SHORT_DATETIME_FORMAT' }} + {% endif %} + +
+ {% csrf_token %} +
+

{% blocktrans %}Pickup Request Confirmation{% endblocktrans %}

+ +
+ +

+ {% blocktrans %}Are you sure you want to confirm the pickup request, + acknowledging that you will be responsible for being pickup?{% endblocktrans %} +

+ + {% if configuration.enable_note_from_matcher %} + + +

{% translate 'What to include in message?' %}

+
    +
  • {% translate 'Through which platform you will contact him/her' %}
  • +
  • {% translate "That you're looking forward to see her/him" %}
  • +
  • {% translate "Kind words to support in their international mobility" %}
  • +
+ {% endif %} + + +
+
+ + +
+
+
+ {% endfor %} +
+ + {% if not object_list.exists %} +
+ No waiting requests to match. Please check back later. +
+ {% endif %} +{% endblock %} diff --git a/fiesta/apps/pickup_system/templates/pickup_system/my_pickups.html b/fiesta/apps/pickup_system/templates/pickup_system/my_pickups.html new file mode 100644 index 00000000..49a3e1d1 --- /dev/null +++ b/fiesta/apps/pickup_system/templates/pickup_system/my_pickups.html @@ -0,0 +1,32 @@ +{% extends "fiesta/base.html" %} +{% load pickup_system %} + +{% load pickup_system %} +{% load breadcrumbs %} +{% load i18n %} + +{% block upper_head %} + {% trans "My Buddies" as title %} + {% breadcrumb_push_current_plugin %} + {% breadcrumb_push_item title %} +{% endblock upper_head %} + +{% block main %} + {% get_pickup_system_configuration as configuration %} + + {% for match in object_list.all %} + {% include "pickup_system/parts/request_match_card.html" with br=match.request title=match.request.issuer.full_name connect_with=match.request.issuer %} + {% empty %} + + {% endfor %} +{% endblock %} diff --git a/fiesta/apps/pickup_system/templates/pickup_system/parts/pickup_request_note_help.html b/fiesta/apps/pickup_system/templates/pickup_system/parts/pickup_request_note_help.html new file mode 100644 index 00000000..ecd1f313 --- /dev/null +++ b/fiesta/apps/pickup_system/templates/pickup_system/parts/pickup_request_note_help.html @@ -0,0 +1,6 @@ +
+

What to include to the requests?

+ +
diff --git a/fiesta/apps/pickup_system/templates/pickup_system/parts/request_match_card.html b/fiesta/apps/pickup_system/templates/pickup_system/parts/request_match_card.html new file mode 100644 index 00000000..892ee874 --- /dev/null +++ b/fiesta/apps/pickup_system/templates/pickup_system/parts/request_match_card.html @@ -0,0 +1,176 @@ +{% load i18n %} +{% load static %} +{% load utils %} +{% load user_profile %} +{% load pickup_system %} +
+
+

+ {{ title }} + + + {{ br.get_state_display }} + +

+ +
+ +
+ {{ br.issuer.get_full_name }} + +
+
+
+ {% get_user_picture br.issuer as issuer_picture %} + + {% if issuer_picture %} + Issuer picture + {% else %} + {{ br.issuer.first_name|first }}{{ br.issuer.last_name|first }} + {% endif %} +
+
+
{{ br.note }}
+ {#
#} + {# {% for interest in br.get_interests_display %}{{ interest }}{% endfor %}#} + {#
#} +
+ + {% if br.state == br.State.MATCHED %} +
+
+ {% if br.match.created %} + + {% endif %} + {{ br.match.matcher.get_full_name }} +
+
+
+ {% get_user_picture br.match.matcher as matcher_picture %} + + {% if matcher_picture %} + Matcher picture + {% else %} + {{ br.match.matcher.first_name|first }}{{ br.match.matcher.last_name|first }} + {% endif %} +
+
+
+ {% if br.match.note %} + {{ br.match.note }} + {% else %} + {% translate "We have been successfully matched!" %} + {% endif %} +
+
+ +
+
+

Connect with {{ connect_with.first_name }}

+
+
+
+ {% if connect_with.profile.telegram %} + + + + + Telegram + + {% endif %} + {% if connect_with.profile.whatsapp %} + + + + + WhatsApp + + {% endif %} + {% if connect_with.profile.facebook %} + + + + + + Facebook + + {% endif %} + {% if connect_with.profile.instagram %} + + + + + Instagram + + {% endif %} + {% if connect_with.primary_email %} + + βœ‰οΈ + E-mail + + {% endif %} +
+ {% elif br.state == br.State.CREATED %} +
+
+
+
πŸ€–
+
+
+ {% get_waiting_pickup_requests_placed_before br as waiting_total %} + +
βŒ› {% translate "Waiting for match" %}
+ + {% if waiting_total %} + There is {{ waiting_total }} waiting request{{ waiting_total|pluralize:"s" }} before yours. + {% endif %} +
+
+ {% endif %} +
+
diff --git a/fiesta/apps/pickup_system/templates/pickup_system/parts/requests_editor_match_btn.html b/fiesta/apps/pickup_system/templates/pickup_system/parts/requests_editor_match_btn.html new file mode 100644 index 00000000..146067f6 --- /dev/null +++ b/fiesta/apps/pickup_system/templates/pickup_system/parts/requests_editor_match_btn.html @@ -0,0 +1,11 @@ +{% load i18n %} + + {% if record.match %} + {% trans "Change pickup" %} + {% else %} + {% trans "Match" %} + {% endif %} + diff --git a/fiesta/apps/pickup_system/templatetags/__init__.py b/fiesta/apps/pickup_system/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fiesta/apps/pickup_system/templatetags/pickup_system.py b/fiesta/apps/pickup_system/templatetags/pickup_system.py new file mode 100644 index 00000000..486a84df --- /dev/null +++ b/fiesta/apps/pickup_system/templatetags/pickup_system.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +import re + +from django import template + +from apps.pickup_system.apps import PickupSystemConfig +from apps.pickup_system.models import PickupRequest +from apps.plugins.middleware.plugin import HttpRequest +from apps.plugins.models import Plugin +from apps.plugins.utils import all_plugins_mapped_to_class + +register = template.Library() + +# I know, it's not the best regex for emails +# [\w.] as [a-zA-Z0-9_.] +CENSOR_REGEX = re.compile( + # emails + r"^$|\S+@\S+\.\S+" + # instagram username + r"|@[\w.]+" + # european phone numbers + r"|\+?\d{1,3}[ \-]?[(]?\d{3,4}[)]?[ \-]?\d{3,4}[ \-]?\d{3,4}", + # URL adresses SIMPLIFIED + # r"(https?://)?([a-z\d_\-]{3,}\.)+[a-z]{2,4}(/\S*)?" + re.VERBOSE | re.IGNORECASE, +) + + +@register.filter +def censor_description(description: str) -> str: + return CENSOR_REGEX.sub("---censored---", description) + + +@register.simple_tag(takes_context=True) +def get_current_pickup_request_of_user(context): + request: HttpRequest = context["request"] + + try: + return request.membership.user.pickup_system_issued_requests.filter( + responsible_section=request.membership.section, + ).latest("created") + except PickupRequest.DoesNotExist: + return None + + +@register.simple_tag(takes_context=True) +def get_waiting_requests_to_match(context): + request: HttpRequest = context["request"] + + # pickup_system_plugin: Plugin = request.membership.section.plugins.get( + # app_label=all_plugins_mapped_to_class().get(PickupSystemConfig).label + # ) + + return PickupRequest.objects.filter( + responsible_section=request.membership.section, + state=PickupRequest.State.CREATED, + ) + + +@register.simple_tag(takes_context=True) +def get_waiting_pickup_requests_placed_before(context, br: PickupRequest): + request: HttpRequest = context["request"] + + return request.membership.section.pickup_system_requests.filter( + state=PickupRequest.State.CREATED, + created__lt=br.created, + ).count() + + +@register.simple_tag(takes_context=True) +def get_matched_pickup_requests(context): + request: HttpRequest = context["request"] + + # TODO: limit by semester / time + return request.user.pickup_system_request_matches.filter( + request__responsible_section=request.membership.section, + request__state=PickupRequest.State.MATCHED, + ).order_by("-created") + + +@register.filter +def request_state_to_css_variant(state: PickupRequest.State): + return { + PickupRequest.State.CREATED: "info", + PickupRequest.State.MATCHED: "success", + PickupRequest.State.CANCELLED: "danger", + }.get(state) + + +@register.simple_tag(takes_context=True) +def get_pickup_system_configuration(context): + request: HttpRequest = context["request"] + + pickup_system_plugin: Plugin = request.in_space_of_section.plugins.get( + app_label=all_plugins_mapped_to_class().get(PickupSystemConfig).label + ) + + return pickup_system_plugin.configuration diff --git a/fiesta/apps/pickup_system/urls.py b/fiesta/apps/pickup_system/urls.py new file mode 100644 index 00000000..68d59411 --- /dev/null +++ b/fiesta/apps/pickup_system/urls.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from django.urls import path + +from ..accounts.models.profile import user_profile_picture_storage +from .views import PickupSystemIndexView +from .views.editor import PickupRequestEditorDetailView, PickupRequestsEditorView, QuickPickupMatchView +from .views.matches import MyPickups +from .views.matching import IssuerPictureServeView, MatcherPictureServeView, MatchingRequestsView, TakePickupRequestView +from .views.request import NewPickupRequestView + +urlpatterns = [ + path("", PickupSystemIndexView.as_view(), name="index"), + path("new-request", NewPickupRequestView.as_view(), name="new-request"), + path("requests", PickupRequestsEditorView.as_view(), name="requests"), + path("my-pickups", MyPickups.as_view(), name="my-pickups"), + path("matching-requests", MatchingRequestsView.as_view(), name="matching-requests"), + path( + "take-request/", + TakePickupRequestView.as_view(), + name="take-pickup-request", + ), + path("detail/", PickupRequestEditorDetailView.as_view(), name="editor-detail"), + path("quick-match/", QuickPickupMatchView.as_view(), name="quick-match"), + # serve profile picture with proxy view + IssuerPictureServeView.as_url(user_profile_picture_storage, url_name="serve-issuer-profile-picture"), + MatcherPictureServeView.as_url(user_profile_picture_storage, url_name="serve-matcher-profile-picture"), +] diff --git a/fiesta/apps/pickup_system/views/__init__.py b/fiesta/apps/pickup_system/views/__init__.py new file mode 100644 index 00000000..da356cad --- /dev/null +++ b/fiesta/apps/pickup_system/views/__init__.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from .index import PickupSystemIndexView + +__all__ = ["PickupSystemIndexView"] diff --git a/fiesta/apps/pickup_system/views/editor.py b/fiesta/apps/pickup_system/views/editor.py new file mode 100644 index 00000000..f7591eec --- /dev/null +++ b/fiesta/apps/pickup_system/views/editor.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from django.contrib.messages.views import SuccessMessageMixin +from django.urls import reverse, reverse_lazy +from django.utils.translation import gettext_lazy as _ +from django.views.generic import UpdateView +from django_tables2 import TemplateColumn +from django_tables2.columns.base import LinkTransform +from django_tables2.utils import Accessor + +from apps.fiestaforms.views.htmx import HtmxFormMixin +from apps.fiestarequests.tables.editor import BaseRequestsFilter, BaseRequestsTable +from apps.fiestarequests.views.editor import BaseQuickRequestMatchView +from apps.fiestatables.views.tables import FiestaTableView +from apps.pickup_system.forms import PickupRequestEditorForm, QuickPickupMatchForm +from apps.pickup_system.models import PickupRequest, PickupRequestMatch +from apps.sections.middleware.section_space import HttpRequest +from apps.sections.views.mixins.membership import EnsurePrivilegedUserViewMixin +from apps.utils.breadcrumbs import with_breadcrumb, with_object_breadcrumb +from apps.utils.views import AjaxViewMixin + + +class PickupRequestsTable(BaseRequestsTable): + match_request = TemplateColumn( + template_name="pickup_system/parts/requests_editor_match_btn.html", + exclude_from_export=True, + order_by="match", + ) + + class Meta(BaseRequestsTable.Meta): + fields = BaseRequestsTable.Meta.fields + ("match_request",) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if "issuer_name" in self.columns: + # sometimes excluded + self.columns["issuer_name"].link = LinkTransform( + attrs={"x-data": lambda: "modal($el.href)", "x-bind": "bind"}, + reverse_args=("pickup_system:editor-detail", {"pk": Accessor("pk")}), + ) + + +@with_breadcrumb(_("Pickup System")) +@with_breadcrumb(_("Requests")) +class PickupRequestsEditorView( + EnsurePrivilegedUserViewMixin, + FiestaTableView, +): + request: HttpRequest + table_class = PickupRequestsTable + filterset_class = BaseRequestsFilter + + def get_queryset(self): + return self.request.in_space_of_section.pickup_system_requests.select_related( + "issuer__profile", + ) + + +@with_breadcrumb(_("Pickup System")) +@with_object_breadcrumb() +class PickupRequestEditorDetailView( + EnsurePrivilegedUserViewMixin, + SuccessMessageMixin, + HtmxFormMixin, + AjaxViewMixin, + UpdateView, +): + template_name = "fiestaforms/pages/card_page_for_ajax_form.html" + ajax_template_name = "fiestaforms/parts/ajax-form-container.html" + model = PickupRequest + form_class = PickupRequestEditorForm + + success_url = reverse_lazy("pickup_system:requests") + success_message = _("Pickup request has been updated.") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["form_url"] = reverse("pickup_system:editor-detail", kwargs={"pk": self.object.pk}) + return context + + +@with_breadcrumb(_("Quick Pickup Match")) +@with_object_breadcrumb() +class QuickPickupMatchView(BaseQuickRequestMatchView): + model = PickupRequest + form_class = QuickPickupMatchForm + + success_url = reverse_lazy("pickup_system:requests") + success_message = _("Pickup request has been matched.") + + form_url = "pickup_system:quick-match" + match_model = PickupRequestMatch diff --git a/fiesta/apps/pickup_system/views/index.py b/fiesta/apps/pickup_system/views/index.py new file mode 100644 index 00000000..6915d298 --- /dev/null +++ b/fiesta/apps/pickup_system/views/index.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from django.http import HttpResponseRedirect +from django.urls import reverse +from django.views.generic import TemplateView + +from apps.pickup_system.models import PickupRequest, PickupSystemConfiguration +from apps.plugins.views import PluginConfigurationViewMixin +from apps.sections.middleware.user_membership import HttpRequest +from apps.sections.views.mixins.section_space import EnsureInSectionSpaceViewMixin + + +class PickupSystemIndexView( + EnsureInSectionSpaceViewMixin, + PluginConfigurationViewMixin[PickupSystemConfiguration], + TemplateView, +): + request: HttpRequest + + extra_context = { + "RequestState": PickupRequest.State, + } + + def get(self, request, *args, **kwargs): + if ( + self.request.membership.is_international + and not self.request.in_space_of_section.pickup_system_requests.filter(issuer=self.request.user).exists() + ): + return HttpResponseRedirect(reverse("pickup_system:wanna-pickup")) + + return super().get(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + data = super().get_context_data(**kwargs) + + data.update( + {"requests": self.request.in_space_of_section.pickup_system_requests.filter(issuer=self.request.user)} + ) + return data + + def get_template_names(self): + return [ + ( + "pickup_system/index_international.html" + if self.request.membership.is_international + else "pickup_system/index_member.html" + ) + ] diff --git a/fiesta/apps/pickup_system/views/matches.py b/fiesta/apps/pickup_system/views/matches.py new file mode 100644 index 00000000..5e4a3558 --- /dev/null +++ b/fiesta/apps/pickup_system/views/matches.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from django.views.generic import ListView + +from apps.plugins.middleware.plugin import HttpRequest +from apps.sections.views.mixins.membership import EnsureLocalUserViewMixin + + +class MyPickups(EnsureLocalUserViewMixin, ListView): + request: HttpRequest + + template_name = "pickup_system/my_pickups.html" + + def get_queryset(self): + return self.request.user.pickup_system_request_matches.prefetch_related( + "request__issuer__profile" + ).select_related("request", "matcher") diff --git a/fiesta/apps/pickup_system/views/matching.py b/fiesta/apps/pickup_system/views/matching.py new file mode 100644 index 00000000..b93db75e --- /dev/null +++ b/fiesta/apps/pickup_system/views/matching.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from django.views.generic import ListView + +from apps.fiestarequests.models.request import BaseRequestProtocol +from apps.fiestarequests.views.matching import BaseTakeRequestView +from apps.pickup_system.models import PickupRequest, PickupRequestMatch, PickupSystemConfiguration +from apps.pickup_system.models.files import BaseIssuerPictureServeView, BaseMatcherPictureServeView +from apps.plugins.middleware.plugin import HttpRequest +from apps.plugins.views import PluginConfigurationViewMixin +from apps.sections.views.mixins.membership import EnsureLocalUserViewMixin +from apps.sections.views.mixins.section_space import EnsureInSectionSpaceViewMixin + + +class MatchingRequestsView( + EnsureInSectionSpaceViewMixin, + EnsureLocalUserViewMixin, + PluginConfigurationViewMixin[PickupSystemConfiguration], + ListView, +): + template_name = "pickup_system/matching_requests.html" + + model = PickupRequest + + def get_queryset(self): + return self.request.in_space_of_section.pickup_system_requests.filter( + state=PickupRequest.State.CREATED, + ) + + +class TakePickupRequestView( + BaseTakeRequestView, +): + match_model = PickupRequestMatch + + def get_queryset(self): + return self.request.in_space_of_section.pickup_system_requests.filter( + state=BaseRequestProtocol.State.CREATED, + ) + + +class ServeFilesFromPickupsMixin: + def get_request_queryset(self, request: HttpRequest): + return request.membership.section.pickup_system_requests + + +class IssuerPictureServeView(ServeFilesFromPickupsMixin, BaseIssuerPictureServeView): + ... + + +class MatcherPictureServeView( + ServeFilesFromPickupsMixin, + BaseMatcherPictureServeView, +): + ... diff --git a/fiesta/apps/pickup_system/views/request.py b/fiesta/apps/pickup_system/views/request.py new file mode 100644 index 00000000..c768c632 --- /dev/null +++ b/fiesta/apps/pickup_system/views/request.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from django.http import HttpResponseRedirect +from django.urls import reverse, reverse_lazy +from django.utils.translation import gettext_lazy as _ +from django.views.generic import TemplateView + +from apps.fiestarequests.views.request import BaseNewRequestView +from apps.pickup_system.forms import NewPickupRequestForm +from apps.pickup_system.models import PickupSystemConfiguration +from apps.plugins.views import PluginConfigurationViewMixin +from apps.sections.views.mixins.section_space import EnsureInSectionSpaceViewMixin + + +class PickupSystemEntrance(EnsureInSectionSpaceViewMixin, PluginConfigurationViewMixin, TemplateView): + def get(self, request, *args, **kwargs): + if self.request.membership.is_international: + return HttpResponseRedirect(reverse("pickup_system:new-request")) + + c: PickupSystemConfiguration = self.configuration + if c.matching_policy_instance.can_member_match: + return HttpResponseRedirect(reverse("pickup_system:matching-requests")) + + return HttpResponseRedirect(reverse("pickup_system:index")) + + +class NewPickupRequestView(BaseNewRequestView): + form_class = NewPickupRequestForm + success_message = _("Your pickup request has been successfully created!") + + success_url = reverse_lazy("pickup_system:index") diff --git a/fiesta/apps/plugins/migrations/0017_alter_plugin_app_label.py b/fiesta/apps/plugins/migrations/0017_alter_plugin_app_label.py new file mode 100644 index 00000000..b0429c42 --- /dev/null +++ b/fiesta/apps/plugins/migrations/0017_alter_plugin_app_label.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.7 on 2023-11-15 23:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('plugins', '0016_alter_plugin_created'), + ] + + operations = [ + migrations.AlterField( + model_name='plugin', + name='app_label', + field=models.CharField(choices=[('buddy_system', 'Buddy System'), ('dashboard', 'Dashboard'), ('sections', 'ESN section'), ('esncards', 'ESNcard'), ('events', 'Events'), ('pages', 'Pages'), ('pickup_system', 'Pickup System')], help_text='Defines system application, which specific plugin represents.', max_length=256, verbose_name='app label'), + ), + ] diff --git a/fiesta/apps/sections/models/section.py b/fiesta/apps/sections/models/section.py index 98c8e9e6..30a9a6b7 100644 --- a/fiesta/apps/sections/models/section.py +++ b/fiesta/apps/sections/models/section.py @@ -136,6 +136,7 @@ def section_home_url(self, for_membership: SectionMembership = None) -> str | No # only typing of related manager buddy_system_requests: models.QuerySet + pickup_system_requests: models.QuerySet class SectionUniversity(BaseTimestampedModel): diff --git a/fiesta/apps/sections/views/membership.py b/fiesta/apps/sections/views/membership.py index b83aeaaa..aef89070 100644 --- a/fiesta/apps/sections/views/membership.py +++ b/fiesta/apps/sections/views/membership.py @@ -12,6 +12,9 @@ from apps.buddy_system.models import BuddyRequest from apps.buddy_system.views.editor import BuddyRequestsTable from apps.fiestatables.views.tables import FiestaMultiTableMixin +from apps.pickup_system.apps import PickupSystemConfig +from apps.pickup_system.models import PickupRequest +from apps.pickup_system.views.editor import PickupRequestsTable from apps.plugins.views.mixins import CheckEnabledPluginsViewMixin from apps.sections.models import SectionMembership from apps.sections.views.mixins.membership import EnsurePrivilegedUserViewMixin @@ -35,6 +38,9 @@ def page_title(view: DetailView | View) -> BreadcrumbItem: ) +clean_list = lambda to_clean: list(filter(None, to_clean)) + + @with_breadcrumb(_("Section")) @with_callable_breadcrumb(getter=page_title) @with_object_breadcrumb(prefix=None, getter=attrgetter("user.full_name")) @@ -51,12 +57,18 @@ class MembershipDetailView( def get_context_data(self, **kwargs): bs_enabled = self._is_plugin_enabled_for_user(BuddySystemConfig) + ps_enabled = self._is_plugin_enabled_for_user(PickupSystemConfig) data = super().get_context_data(**kwargs) data.update( { - "table_titles": ( - (_("πŸ§‘β€πŸ€β€πŸ§‘ Buddies") if self.object.is_local else _("πŸ§‘β€πŸ€β€πŸ§‘ Buddy requests"),) if bs_enabled else () + "table_titles": clean_list( + ( + (_("πŸ§‘β€πŸ€β€πŸ§‘ Buddies") if self.object.is_local else _("πŸ§‘β€πŸ€β€πŸ§‘ Buddy requests")) + if bs_enabled + else None, + (_("πŸ“ Pickups") if self.object.is_local else _("πŸ“ Pickup requests")) if ps_enabled else None, + ) ), } ) @@ -64,9 +76,10 @@ def get_context_data(self, **kwargs): def get_tables(self): bs_enabled = self._is_plugin_enabled_for_user(BuddySystemConfig) + ps_enabled = self._is_plugin_enabled_for_user(PickupSystemConfig) if self.object.is_local: - return ( + return clean_list( [ BuddyRequestsTable( request=self.request, @@ -76,13 +89,24 @@ def get_tables(self): "matcher_picture", "match_request", ), - ), + ) + if bs_enabled + else None, + PickupRequestsTable( + request=self.request, + data=PickupRequest.objects.filter(match__matcher=self.object.user), + exclude=( + "matcher_name", + "matcher_picture", + "match_request", + ), + ) + if ps_enabled + else None, ] - if bs_enabled - else [] ) - return ( + return clean_list( [ BuddyRequestsTable( request=self.request, @@ -93,10 +117,22 @@ def get_tables(self): # "matcher_picture", # "match_request", ), - ), + ) + if bs_enabled + else None, + PickupRequestsTable( + request=self.request, + data=PickupRequest.objects.filter(issuer=self.object.user), + exclude=( + "issuer_name", + "issuer_picture", + # "matcher_picture", + # "match_request", + ), + ) + if ps_enabled + else None, ] - if bs_enabled - else [] ) def get_queryset(self): diff --git a/fiesta/fiesta/settings/project.py b/fiesta/fiesta/settings/project.py index 5bbfbe9d..2bdec62a 100644 --- a/fiesta/fiesta/settings/project.py +++ b/fiesta/fiesta/settings/project.py @@ -63,6 +63,7 @@ def ALLOWED_HOSTS(self): # Fiesta apps "apps.accounts.apps.AccountsConfig", "apps.buddy_system.apps.BuddySystemConfig", + "apps.pickup_system.apps.PickupSystemConfig", "apps.dashboard.apps.DashboardConfig", "apps.esncards.apps.ESNcardsConfig", "apps.fiestaforms.apps.FiestaFormsConfig", From 0080f2d0f3a6dda09ac63c0c2c69a721c4baef41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josef=20Kol=C3=A1=C5=99?= Date: Thu, 16 Nov 2023 11:10:16 +0100 Subject: [PATCH 2/7] feat(ps): styles & hidden not yet working matching policies --- .../buddy_system/dashboard_block.html | 2 +- fiesta/apps/fiestarequests/matching_policy.py | 5 ++-- .../fiestarequests/models/configuration.py | 3 +- .../pickup_system/dashboard_block.html | 28 ++++--------------- 4 files changed, 11 insertions(+), 27 deletions(-) diff --git a/fiesta/apps/buddy_system/templates/buddy_system/dashboard_block.html b/fiesta/apps/buddy_system/templates/buddy_system/dashboard_block.html index 25a0e1ae..e6c957cf 100644 --- a/fiesta/apps/buddy_system/templates/buddy_system/dashboard_block.html +++ b/fiesta/apps/buddy_system/templates/buddy_system/dashboard_block.html @@ -45,7 +45,7 @@
βœ… Matched
-
+
It's a match!
You have been matched with {{ br.match.matcher.full_name }}. diff --git a/fiesta/apps/fiestarequests/matching_policy.py b/fiesta/apps/fiestarequests/matching_policy.py index d9d39ae8..cb257ef0 100644 --- a/fiesta/apps/fiestarequests/matching_policy.py +++ b/fiesta/apps/fiestarequests/matching_policy.py @@ -97,8 +97,9 @@ class MatchingPoliciesRegister: AVAILABLE_POLICIES = [ ManualByEditorMatchingPolicy, ManualByMemberMatchingPolicy, - SameFacultyMatchingPolicy, - LimitedSameFacultyMatchingPolicy, + # TODO: make it working + # SameFacultyMatchingPolicy, + # LimitedSameFacultyMatchingPolicy, # AutoMatchingPolicy ] diff --git a/fiesta/apps/fiestarequests/models/configuration.py b/fiesta/apps/fiestarequests/models/configuration.py index 20e1eadd..3380ab81 100644 --- a/fiesta/apps/fiestarequests/models/configuration.py +++ b/fiesta/apps/fiestarequests/models/configuration.py @@ -14,7 +14,8 @@ class BaseRequestSystemConfiguration(BasePluginConfiguration): display_issuer_faculty = models.BooleanField(default=False) display_request_creation_date = models.BooleanField(default=True) - rolling_limit = models.PositiveSmallIntegerField(default=0) + # TODO: make it visible and working + rolling_limit = models.PositiveSmallIntegerField(default=0, editable=False) enable_note_from_matcher = models.BooleanField( default=True, diff --git a/fiesta/apps/pickup_system/templates/pickup_system/dashboard_block.html b/fiesta/apps/pickup_system/templates/pickup_system/dashboard_block.html index 64b1f2ed..63188e23 100644 --- a/fiesta/apps/pickup_system/templates/pickup_system/dashboard_block.html +++ b/fiesta/apps/pickup_system/templates/pickup_system/dashboard_block.html @@ -6,7 +6,7 @@ {% get_pickup_system_configuration as configuration %}
-
+
Pickup System
{% if request.membership.is_international %} {% get_current_pickup_request_of_user as br %} @@ -24,32 +24,14 @@
{% elif br.state == br.State.MATCHED %} {% get_user_picture br.match.matcher as pickup_picture %} -
+
βœ… Pickup confirmed
+
{{ br.match.matcher.full_name }} is ready to pick you up.
+
{% trans "find out about your pickup" %} - -
-
- {% if pickup_picture %} - Matched pickup picture - {% else %} - {{ br.match.matcher.first_name|first }}{{ br.match.matcher.last_name|first }} - {% endif %} -
-
-
-
βœ… Matched
-
- It's a match! -
- You have been matched with {{ br.match.matcher.full_name }}. -
+ {% endif %} {% else %} {% get_waiting_requests_to_match as waiting_brs %} From 6600be63dc01446ce4ed4c79e6b7c06127957101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josef=20Kol=C3=A1=C5=99?= Date: Thu, 16 Nov 2023 12:03:52 +0100 Subject: [PATCH 3/7] feat(requests): same faculty matching --- fiesta/apps/fiestarequests/matching_policy.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fiesta/apps/fiestarequests/matching_policy.py b/fiesta/apps/fiestarequests/matching_policy.py index cb257ef0..85c23c2b 100644 --- a/fiesta/apps/fiestarequests/matching_policy.py +++ b/fiesta/apps/fiestarequests/matching_policy.py @@ -57,9 +57,9 @@ class ManualByMemberMatchingPolicy(MatchingPolicyProtocol): can_member_match = True -class SameFacultyMatchingPolicy(MatchingPolicyProtocol): +class ManualWithSameFacultyMatchingPolicy(MatchingPolicyProtocol): id = "same-faculty" - title = _("Restricted to same faculty") + title = _("Manual by members with restriction to same faculty") description = _("Matching is done manually by members themselves, but limited to the same faculty.") can_member_match = True @@ -69,7 +69,7 @@ def limit_requests(self, qs: QuerySet[BuddyRequest], membership: SectionMembersh member_profile: UserProfile = membership.user.profile_or_none return qs.filter(self._base_filter(membership=membership)).filter( - issuer__profile__faculty=member_profile.faculty, + issuer_faculty=member_profile.faculty, ) @@ -97,8 +97,8 @@ class MatchingPoliciesRegister: AVAILABLE_POLICIES = [ ManualByEditorMatchingPolicy, ManualByMemberMatchingPolicy, + ManualWithSameFacultyMatchingPolicy, # TODO: make it working - # SameFacultyMatchingPolicy, # LimitedSameFacultyMatchingPolicy, # AutoMatchingPolicy ] From 583d9a00978e02a3658a8b11dc649a8b51b797aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josef=20Kol=C3=A1=C5=99?= Date: Thu, 16 Nov 2023 12:44:22 +0100 Subject: [PATCH 4/7] feat(requests): new BaseNewRequestForm --- fiesta/apps/buddy_system/forms.py | 45 +++++-------------- fiesta/apps/fiestarequests/forms/__init__.py | 0 fiesta/apps/fiestarequests/forms/request.py | 45 +++++++++++++++++++ fiesta/apps/fiestarequests/views/request.py | 8 ++-- fiesta/apps/pickup_system/forms.py | 46 ++++++-------------- 5 files changed, 74 insertions(+), 70 deletions(-) create mode 100644 fiesta/apps/fiestarequests/forms/__init__.py create mode 100644 fiesta/apps/fiestarequests/forms/request.py diff --git a/fiesta/apps/buddy_system/forms.py b/fiesta/apps/buddy_system/forms.py index 171a61dc..7385c047 100644 --- a/fiesta/apps/buddy_system/forms.py +++ b/fiesta/apps/buddy_system/forms.py @@ -1,7 +1,7 @@ from __future__ import annotations from django.core.exceptions import ValidationError -from django.forms import BooleanField, HiddenInput, fields_for_model +from django.forms import fields_for_model from django.template.loader import render_to_string from django.utils.functional import lazy from django.utils.translation import gettext_lazy as _ @@ -10,7 +10,8 @@ from apps.buddy_system.models import BuddyRequest, BuddyRequestMatch from apps.fiestaforms.fields.array import ChoicedArrayField from apps.fiestaforms.forms import BaseModelForm -from apps.fiestaforms.widgets.models import ActiveLocalMembersFromSectionWidget, FacultyWidget, UserWidget +from apps.fiestaforms.widgets.models import ActiveLocalMembersFromSectionWidget, UserWidget +from apps.fiestarequests.forms.request import BaseNewRequestForm USER_PROFILE_CONTACT_FIELDS = fields_for_model( UserProfile, @@ -18,52 +19,28 @@ ) -class NewBuddyRequestForm(BaseModelForm): +class NewBuddyRequestForm(BaseNewRequestForm): submit_text = _("Send request for buddy") - # TODO: group field somehow and add group headings - facebook, instagram, telegram, whatsapp = USER_PROFILE_CONTACT_FIELDS.values() - - approving_request = BooleanField(required=True, label=_("I really want a buddy")) - - class Meta: + class Meta(BaseNewRequestForm.Meta): model = BuddyRequest - fields = ( - "note", - "interests", - "responsible_section", - "issuer", - "issuer_faculty", - ) - field_classes = { + + fields = BaseNewRequestForm.Meta.fields + ("interests",) + field_classes = BaseNewRequestForm.Meta.field_classes | { "interests": ChoicedArrayField, } - widgets = { - "responsible_section": HiddenInput, - "issuer": HiddenInput, - "issuer_faculty": FacultyWidget, - } - labels = { + labels = BaseNewRequestForm.Meta.labels | { "note": _("Tell us about yourself"), "interests": _("What are you into?"), - "issuer_faculty": _("Your faculty"), + "approving_requests": _("I really want a buddy"), } - help_texts = { + help_texts = BaseNewRequestForm.Meta.help_texts | { "note": lazy( lambda: render_to_string("buddy_system/parts/buddy_request_note_help.html"), str, ) } - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - if self.initial.get("issuer_faculty"): - self.fields["issuer_faculty"].disabled = True - - -# TODO: add save/load of contacts to/from user_profile - class BuddyRequestEditorForm(BaseModelForm): submit_text = _("Save") diff --git a/fiesta/apps/fiestarequests/forms/__init__.py b/fiesta/apps/fiestarequests/forms/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fiesta/apps/fiestarequests/forms/request.py b/fiesta/apps/fiestarequests/forms/request.py new file mode 100644 index 00000000..e2300b13 --- /dev/null +++ b/fiesta/apps/fiestarequests/forms/request.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from django.forms import BooleanField, fields_for_model +from django.utils.translation import gettext_lazy as _ + +from apps.accounts.models import UserProfile +from apps.fiestaforms.forms import BaseModelForm +from apps.fiestaforms.widgets.models import FacultyWidget + +USER_PROFILE_CONTACT_FIELDS = fields_for_model( + UserProfile, + fields=("facebook", "instagram", "telegram", "whatsapp"), +) + + +class BaseNewRequestForm(BaseModelForm): + submit_text = _("Send request for buddy") + + # TODO: group field somehow and add group headings + facebook, instagram, telegram, whatsapp = USER_PROFILE_CONTACT_FIELDS.values() + + approving_request = BooleanField(required=True) + + class Meta: + fields = ( + "note", + "issuer_faculty", + ) + field_classes = {} + widgets = { + "issuer_faculty": FacultyWidget, + } + labels = { + "note": _("Tell us about yourself"), + "issuer_faculty": _("Your faculty"), + } + help_texts = {} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if self.initial.get("issuer_faculty"): + self.fields["issuer_faculty"].disabled = True + + # TODO: add save/load of contacts to/from user_profile diff --git a/fiesta/apps/fiestarequests/views/request.py b/fiesta/apps/fiestarequests/views/request.py index db7c35e2..d37a854b 100644 --- a/fiesta/apps/fiestarequests/views/request.py +++ b/fiesta/apps/fiestarequests/views/request.py @@ -24,10 +24,7 @@ class BaseNewRequestView( def get_initial(self): p: UserProfile = self.request.user.profile_or_none return { - "responsible_section": self.request.in_space_of_section, - "issuer": self.request.user, "issuer_faculty": p.faculty if p else None, - # "interests": p.interests if p else None, } def form_valid(self, form): @@ -35,4 +32,9 @@ def form_valid(self, form): form.instance.responsible_section = self.request.in_space_of_section form.instance.issuer = self.request.user + p: UserProfile = self.request.user.profile_or_none + if p and not p.faculty: + p.faculty = form.cleaned_data["issuer_faculty"] + p.save(update_fields=["faculty"]) + return super().form_valid(form) diff --git a/fiesta/apps/pickup_system/forms.py b/fiesta/apps/pickup_system/forms.py index adf80931..a3f62f95 100644 --- a/fiesta/apps/pickup_system/forms.py +++ b/fiesta/apps/pickup_system/forms.py @@ -1,14 +1,15 @@ from __future__ import annotations from django.core.exceptions import ValidationError -from django.forms import BooleanField, HiddenInput, fields_for_model +from django.forms import fields_for_model from django.template.loader import render_to_string from django.utils.functional import lazy from django.utils.translation import gettext_lazy as _ from apps.accounts.models import User, UserProfile from apps.fiestaforms.forms import BaseModelForm -from apps.fiestaforms.widgets.models import ActiveLocalMembersFromSectionWidget, FacultyWidget, UserWidget +from apps.fiestaforms.widgets.models import ActiveLocalMembersFromSectionWidget, UserWidget +from apps.fiestarequests.forms.request import BaseNewRequestForm from apps.pickup_system.models import PickupRequest, PickupRequestMatch USER_PROFILE_CONTACT_FIELDS = fields_for_model( @@ -17,47 +18,26 @@ ) -class NewPickupRequestForm(BaseModelForm): +class NewPickupRequestForm(BaseNewRequestForm): submit_text = _("Send request for pickup") - # TODO: group field somehow and add group headings - facebook, instagram, telegram, whatsapp = USER_PROFILE_CONTACT_FIELDS.values() - - approving_request = BooleanField(required=True, label=_("I really want a pickup")) - - class Meta: + class Meta(BaseNewRequestForm.Meta): model = PickupRequest - fields = ( - "note", - # "interests", - "issuer", - "issuer_faculty", - ) - field_classes = { - # "interests": ChoicedArrayField, - } - widgets = { - "issuer": HiddenInput, - "issuer_faculty": FacultyWidget, - } - labels = { - "note": _("Tell us about yourself"), - # "interests": _("What are you into?"), - "issuer_faculty": _("Your faculty"), + + fields = BaseNewRequestForm.Meta.fields + () + field_classes = BaseNewRequestForm.Meta.field_classes | {} + labels = BaseNewRequestForm.Meta.labels | { + "note": _("Tell me details TODO"), + "interests": _("What are you into?"), + "approving_request": _("I really want a pickup"), } - help_texts = { + help_texts = BaseNewRequestForm.Meta.help_texts | { "note": lazy( lambda: render_to_string("pickup_system/parts/pickup_request_note_help.html"), str, ) } - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - if self.initial.get("issuer_faculty"): - self.fields["issuer_faculty"].disabled = True - # TODO: add save/load of contacts to/from user_profile From 39d37010699e64ae984f9ba412c096c6c5456243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josef=20Kol=C3=A1=C5=99?= Date: Thu, 16 Nov 2023 13:17:49 +0100 Subject: [PATCH 5/7] feat(requests): new BaseRequestEditorForm and BaseQuickMatchForm --- fiesta/apps/buddy_system/forms.py | 54 ++++--------------- fiesta/apps/buddy_system/views/matching.py | 12 +++-- fiesta/apps/fiestarequests/forms/editor.py | 54 +++++++++++++++++++ fiesta/apps/pickup_system/forms.py | 59 ++++----------------- fiesta/apps/pickup_system/views/matching.py | 3 +- 5 files changed, 83 insertions(+), 99 deletions(-) create mode 100644 fiesta/apps/fiestarequests/forms/editor.py diff --git a/fiesta/apps/buddy_system/forms.py b/fiesta/apps/buddy_system/forms.py index 7385c047..ba503e9b 100644 --- a/fiesta/apps/buddy_system/forms.py +++ b/fiesta/apps/buddy_system/forms.py @@ -1,16 +1,14 @@ from __future__ import annotations -from django.core.exceptions import ValidationError from django.forms import fields_for_model from django.template.loader import render_to_string from django.utils.functional import lazy from django.utils.translation import gettext_lazy as _ -from apps.accounts.models import User, UserProfile +from apps.accounts.models import UserProfile from apps.buddy_system.models import BuddyRequest, BuddyRequestMatch from apps.fiestaforms.fields.array import ChoicedArrayField -from apps.fiestaforms.forms import BaseModelForm -from apps.fiestaforms.widgets.models import ActiveLocalMembersFromSectionWidget, UserWidget +from apps.fiestarequests.forms.editor import BaseQuickMatchForm, BaseRequestEditorForm from apps.fiestarequests.forms.request import BaseNewRequestForm USER_PROFILE_CONTACT_FIELDS = fields_for_model( @@ -42,58 +40,24 @@ class Meta(BaseNewRequestForm.Meta): } -class BuddyRequestEditorForm(BaseModelForm): - submit_text = _("Save") - +class BuddyRequestEditorForm(BaseRequestEditorForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields["issuer"].disabled = True - if self.instance.state != BuddyRequest.State.CREATED: - # self.fields["matched_by"].disabled = True - # self.fields["matched_at"].disabled = True - self.fields["note"].disabled = True self.fields["interests"].disabled = True - class Meta: + class Meta(BaseRequestEditorForm.Meta): model = BuddyRequest - fields = ( - "issuer", - "state", - "note", - "interests", - # "matched_by", - # "matched_at", - ) - field_classes = { + fields = BaseRequestEditorForm.Meta.fields + ("interests",) + field_classes = BaseRequestEditorForm.Meta.field_classes | { "interests": ChoicedArrayField, - # "matched_at": DateTimeLocalField, - } - widgets = { - "issuer": UserWidget, - # "matched_by": ActiveLocalMembersFromSectionWidget, } + widgets = BaseRequestEditorForm.Meta.widgets | {} -class QuickBuddyMatchForm(BaseModelForm): - submit_text = _("Match") +class QuickBuddyMatchForm(BaseQuickMatchForm): instance: BuddyRequestMatch - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - class Meta: + class Meta(BaseQuickMatchForm.Meta): model = BuddyRequestMatch - fields = ("matcher",) - widgets = { - "matcher": ActiveLocalMembersFromSectionWidget, - } - - def clean_matcher(self): - matcher: User = self.cleaned_data["matcher"] - - if not matcher.profile_or_none.faculty: - raise ValidationError(_("This user has not set their faculty. Please ask them to do so or do it yourself.")) - - return matcher diff --git a/fiesta/apps/buddy_system/views/matching.py b/fiesta/apps/buddy_system/views/matching.py index fd8c10f8..c83ddc4b 100644 --- a/fiesta/apps/buddy_system/views/matching.py +++ b/fiesta/apps/buddy_system/views/matching.py @@ -6,7 +6,7 @@ from apps.buddy_system.models import BuddyRequest, BuddyRequestMatch, BuddySystemConfiguration from apps.fiestarequests.views.matching import BaseTakeRequestView from apps.pickup_system.models.files import BaseIssuerPictureServeView, BaseMatcherPictureServeView -from apps.pickup_system.views.matching import ServeFilesFromPickupsMixin +from apps.plugins.middleware.plugin import HttpRequest from apps.plugins.views import PluginConfigurationViewMixin from apps.sections.views.mixins.membership import EnsureLocalUserViewMixin from apps.sections.views.mixins.section_space import EnsureInSectionSpaceViewMixin @@ -50,12 +50,18 @@ def get_queryset(self): ) -class IssuerPictureServeView(ServeFilesFromPickupsMixin, BaseIssuerPictureServeView): +class ServeFilesFromBuddiesMixin: + @classmethod + def get_request_queryset(cls, request: HttpRequest): + return request.membership.section.buddy_system_requests + + +class IssuerPictureServeView(ServeFilesFromBuddiesMixin, BaseIssuerPictureServeView): ... class MatcherPictureServeView( - ServeFilesFromPickupsMixin, + ServeFilesFromBuddiesMixin, BaseMatcherPictureServeView, ): ... diff --git a/fiesta/apps/fiestarequests/forms/editor.py b/fiesta/apps/fiestarequests/forms/editor.py new file mode 100644 index 00000000..7908ddd8 --- /dev/null +++ b/fiesta/apps/fiestarequests/forms/editor.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ + +from apps.accounts.models import User +from apps.fiestaforms.forms import BaseModelForm +from apps.fiestaforms.widgets.models import ActiveLocalMembersFromSectionWidget, UserWidget +from apps.fiestarequests.models.request import BaseRequestMatchProtocol, BaseRequestProtocol + + +class BaseRequestEditorForm(BaseModelForm): + submit_text = _("Save") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.fields["issuer"].disabled = True + + if self.instance.state != BaseRequestProtocol.State.CREATED: + self.fields["note"].disabled = True + + class Meta: + fields = ( + "issuer", + "state", + "note", + ) + field_classes = {} + widgets = { + "issuer": UserWidget, + } + + +class BaseQuickMatchForm(BaseModelForm): + submit_text = _("Match") + instance: BaseRequestMatchProtocol + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + class Meta: + fields = ("matcher",) + widgets = { + "matcher": ActiveLocalMembersFromSectionWidget, + } + + def clean_matcher(self): + matcher: User = self.cleaned_data["matcher"] + + if not matcher.profile_or_none.faculty: + raise ValidationError(_("This user has not set their faculty. Please ask them to do so or do it yourself.")) + + return matcher diff --git a/fiesta/apps/pickup_system/forms.py b/fiesta/apps/pickup_system/forms.py index a3f62f95..d106da33 100644 --- a/fiesta/apps/pickup_system/forms.py +++ b/fiesta/apps/pickup_system/forms.py @@ -1,14 +1,12 @@ from __future__ import annotations -from django.core.exceptions import ValidationError from django.forms import fields_for_model from django.template.loader import render_to_string from django.utils.functional import lazy from django.utils.translation import gettext_lazy as _ -from apps.accounts.models import User, UserProfile -from apps.fiestaforms.forms import BaseModelForm -from apps.fiestaforms.widgets.models import ActiveLocalMembersFromSectionWidget, UserWidget +from apps.accounts.models import UserProfile +from apps.fiestarequests.forms.editor import BaseQuickMatchForm, BaseRequestEditorForm from apps.fiestarequests.forms.request import BaseNewRequestForm from apps.pickup_system.models import PickupRequest, PickupRequestMatch @@ -42,58 +40,19 @@ class Meta(BaseNewRequestForm.Meta): # TODO: add save/load of contacts to/from user_profile -class PickupRequestEditorForm(BaseModelForm): - submit_text = _("Save") - +class PickupRequestEditorForm(BaseRequestEditorForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields["issuer"].disabled = True - - if self.instance.state != PickupRequest.State.CREATED: - # self.fields["matched_by"].disabled = True - # self.fields["matched_at"].disabled = True - self.fields["note"].disabled = True - # self.fields["interests"].disabled = True - - class Meta: + class Meta(BaseRequestEditorForm.Meta): model = PickupRequest - fields = ( - "issuer", - "state", - "note", - # "interests", - # "matched_by", - # "matched_at", - ) - field_classes = { - # "interests": ChoicedArrayField, - # "matched_at": DateTimeLocalField, - } - widgets = { - "issuer": UserWidget, - # "matched_by": ActiveLocalMembersFromSectionWidget, - } + fields = BaseRequestEditorForm.Meta.fields + () + field_classes = BaseRequestEditorForm.Meta.field_classes | {} + widgets = BaseRequestEditorForm.Meta.widgets | {} -class QuickPickupMatchForm(BaseModelForm): - submit_text = _("Match") +class QuickPickupMatchForm(BaseQuickMatchForm): instance: PickupRequestMatch - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - class Meta: + class Meta(BaseQuickMatchForm.Meta): model = PickupRequestMatch - fields = ("matcher",) - widgets = { - "matcher": ActiveLocalMembersFromSectionWidget, - } - - def clean_matcher(self): - matcher: User = self.cleaned_data["matcher"] - - if not matcher.profile_or_none.faculty: - raise ValidationError(_("This user has not set their faculty. Please ask them to do so or do it yourself.")) - - return matcher diff --git a/fiesta/apps/pickup_system/views/matching.py b/fiesta/apps/pickup_system/views/matching.py index b93db75e..e7e2f102 100644 --- a/fiesta/apps/pickup_system/views/matching.py +++ b/fiesta/apps/pickup_system/views/matching.py @@ -40,7 +40,8 @@ def get_queryset(self): class ServeFilesFromPickupsMixin: - def get_request_queryset(self, request: HttpRequest): + @classmethod + def get_request_queryset(cls, request: HttpRequest): return request.membership.section.pickup_system_requests From b404ad2f052c6ea7afa97976f73e84e5a5529ed7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josef=20Kol=C3=A1=C5=99?= Date: Thu, 16 Nov 2023 13:44:47 +0100 Subject: [PATCH 6/7] feat(fe): upgraded daisyui --- fiesta/apps/pickup_system/views/index.py | 2 +- .../public/templates/public/pages/team.html | 14 +- webpack/package.json | 2 +- webpack/src/css/_Forms.css | 5 + webpack/yarn.lock | 633 +++++++++--------- 5 files changed, 325 insertions(+), 331 deletions(-) diff --git a/fiesta/apps/pickup_system/views/index.py b/fiesta/apps/pickup_system/views/index.py index 6915d298..543998c9 100644 --- a/fiesta/apps/pickup_system/views/index.py +++ b/fiesta/apps/pickup_system/views/index.py @@ -26,7 +26,7 @@ def get(self, request, *args, **kwargs): self.request.membership.is_international and not self.request.in_space_of_section.pickup_system_requests.filter(issuer=self.request.user).exists() ): - return HttpResponseRedirect(reverse("pickup_system:wanna-pickup")) + return HttpResponseRedirect(reverse("pickup_system:new-request")) return super().get(request, *args, **kwargs) diff --git a/fiesta/apps/public/templates/public/pages/team.html b/fiesta/apps/public/templates/public/pages/team.html index 0976c750..c65a0ac9 100644 --- a/fiesta/apps/public/templates/public/pages/team.html +++ b/fiesta/apps/public/templates/public/pages/team.html @@ -14,20 +14,18 @@

{% for _ in "1234" %}
- team +

-
+
-
-
+
+
-
+
- + Date: Thu, 16 Nov 2023 17:29:12 +0100 Subject: [PATCH 7/7] feat(pickups): map selector, time --- ...mconfiguration_matching_policy_and_more.py | 23 ++++++++++++ fiesta/apps/fiestaforms/forms.py | 14 ++++++- .../templates/fiestaforms/classic.html | 4 ++ .../templates/location_field/map_widget.html | 6 +++ fiesta/apps/fiestarequests/tables/editor.py | 4 +- fiesta/apps/pickup_system/forms.py | 30 ++++++++++++--- ...t_location_pickuprequest_place_and_more.py | 37 +++++++++++++++++++ fiesta/apps/pickup_system/models/request.py | 20 +++++++++- .../parts/request_match_card.html | 32 ++++++++++++++-- fiesta/apps/pickup_system/views/editor.py | 14 ++++++- fiesta/fiesta/settings/project.py | 8 ++++ poetry.lock | 13 ++++++- pyproject.toml | 1 + webpack/package.json | 1 + webpack/src/jquery.js | 3 ++ webpack/webpack.base.config.js | 3 ++ webpack/yarn.lock | 5 +++ 17 files changed, 204 insertions(+), 14 deletions(-) create mode 100644 fiesta/apps/buddy_system/migrations/0027_alter_buddysystemconfiguration_matching_policy_and_more.py create mode 100644 fiesta/apps/fiestaforms/templates/location_field/map_widget.html create mode 100644 fiesta/apps/pickup_system/migrations/0002_pickuprequest_location_pickuprequest_place_and_more.py create mode 100644 webpack/src/jquery.js diff --git a/fiesta/apps/buddy_system/migrations/0027_alter_buddysystemconfiguration_matching_policy_and_more.py b/fiesta/apps/buddy_system/migrations/0027_alter_buddysystemconfiguration_matching_policy_and_more.py new file mode 100644 index 00000000..b6f39952 --- /dev/null +++ b/fiesta/apps/buddy_system/migrations/0027_alter_buddysystemconfiguration_matching_policy_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.7 on 2023-11-16 15:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('buddy_system', '0026_alter_buddyrequest_created_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='buddysystemconfiguration', + name='matching_policy', + field=models.CharField(choices=[('manual-by-editor', 'Manual by editors'), ('manual-by-member', 'Manual by members'), ('same-faculty', 'Manual by members with restriction to same faculty')], default='manual-by-editor', help_text='Manual by editors: Matching is done manually only by editors.
Manual by members: Matching is done manually directly by members.
Manual by members with restriction to same faculty: Matching is done manually by members themselves, but limited to the same faculty.', max_length=32), + ), + migrations.AlterField( + model_name='buddysystemconfiguration', + name='rolling_limit', + field=models.PositiveSmallIntegerField(default=0, editable=False), + ), + ] diff --git a/fiesta/apps/fiestaforms/forms.py b/fiesta/apps/fiestaforms/forms.py index 8ba6f76d..69cd94ae 100644 --- a/fiesta/apps/fiestaforms/forms.py +++ b/fiesta/apps/fiestaforms/forms.py @@ -1,7 +1,8 @@ from __future__ import annotations -from django.forms import DateInput as DjDateInput, Form, ModelForm +from django.forms import DateInput as DjDateInput, Form, Media, ModelForm from django.utils.translation import gettext_lazy as _ +from webpack_loader.utils import get_files class DateInput(DjDateInput): @@ -24,3 +25,14 @@ class BaseForm(Form): @property def base_form_class(self): return BaseForm + + +class WebpackMediaFormMixin: + _webpack_bundle: str + + @property + def media(self): + media = super().media + media += Media(js=[f["url"] for f in get_files(self._webpack_bundle)]) + + return media diff --git a/fiesta/apps/fiestaforms/templates/fiestaforms/classic.html b/fiesta/apps/fiestaforms/templates/fiestaforms/classic.html index 93af23d8..adab3c0c 100644 --- a/fiesta/apps/fiestaforms/templates/fiestaforms/classic.html +++ b/fiesta/apps/fiestaforms/templates/fiestaforms/classic.html @@ -2,6 +2,10 @@ {% load fiestaforms %} {% load as_widget_field as_label from fiestaforms %}
+ {{ form.media }} + + {% for f in form.fields.values %}{{ f.widget.media|default:"" }}{% endfor %} + {% for bf, errors in fields %} {% include "fiestaforms/parts/field.html" with bf=bf errors=errors %} {% endfor %} diff --git a/fiesta/apps/fiestaforms/templates/location_field/map_widget.html b/fiesta/apps/fiestaforms/templates/location_field/map_widget.html new file mode 100644 index 00000000..6d31a88e --- /dev/null +++ b/fiesta/apps/fiestaforms/templates/location_field/map_widget.html @@ -0,0 +1,6 @@ + +
+ {# #} + +
+
diff --git a/fiesta/apps/fiestarequests/tables/editor.py b/fiesta/apps/fiestarequests/tables/editor.py index bde29574..d8f596fc 100644 --- a/fiesta/apps/fiestarequests/tables/editor.py +++ b/fiesta/apps/fiestarequests/tables/editor.py @@ -94,12 +94,14 @@ class Meta: "issuer_name", "issuer_picture", "state", + "...", "matcher_name", "matcher_picture", "requested", "matched", - "...", + "match_request", ) + empty_text = _("No requests found") attrs = dict(tbody={"hx-disable": True}) diff --git a/fiesta/apps/pickup_system/forms.py b/fiesta/apps/pickup_system/forms.py index d106da33..5799653b 100644 --- a/fiesta/apps/pickup_system/forms.py +++ b/fiesta/apps/pickup_system/forms.py @@ -6,6 +6,8 @@ from django.utils.translation import gettext_lazy as _ from apps.accounts.models import UserProfile +from apps.fiestaforms.fields.datetime import DateTimeLocalField +from apps.fiestaforms.forms import WebpackMediaFormMixin from apps.fiestarequests.forms.editor import BaseQuickMatchForm, BaseRequestEditorForm from apps.fiestarequests.forms.request import BaseNewRequestForm from apps.pickup_system.models import PickupRequest, PickupRequestMatch @@ -16,18 +18,30 @@ ) -class NewPickupRequestForm(BaseNewRequestForm): +class NewPickupRequestForm(WebpackMediaFormMixin, BaseNewRequestForm): + _webpack_bundle = "jquery" submit_text = _("Send request for pickup") class Meta(BaseNewRequestForm.Meta): model = PickupRequest - fields = BaseNewRequestForm.Meta.fields + () - field_classes = BaseNewRequestForm.Meta.field_classes | {} + fields = ( + ( + "time", + "place", + "location", + ) + + BaseNewRequestForm.Meta.fields + + () + ) + field_classes = BaseNewRequestForm.Meta.field_classes | {"time": DateTimeLocalField} + widgets = BaseNewRequestForm.Meta.widgets | {} labels = BaseNewRequestForm.Meta.labels | { "note": _("Tell me details TODO"), "interests": _("What are you into?"), "approving_request": _("I really want a pickup"), + "place": _("Where do you want to be picked up?"), + "location": _("Place marker as accurately as possible"), } help_texts = BaseNewRequestForm.Meta.help_texts | { "note": lazy( @@ -40,13 +54,19 @@ class Meta(BaseNewRequestForm.Meta): # TODO: add save/load of contacts to/from user_profile -class PickupRequestEditorForm(BaseRequestEditorForm): +class PickupRequestEditorForm(WebpackMediaFormMixin, BaseRequestEditorForm): + _webpack_bundle = "jquery" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) class Meta(BaseRequestEditorForm.Meta): model = PickupRequest - fields = BaseRequestEditorForm.Meta.fields + () + fields = BaseRequestEditorForm.Meta.fields + ( + "time", + "place", + "location", + ) field_classes = BaseRequestEditorForm.Meta.field_classes | {} widgets = BaseRequestEditorForm.Meta.widgets | {} diff --git a/fiesta/apps/pickup_system/migrations/0002_pickuprequest_location_pickuprequest_place_and_more.py b/fiesta/apps/pickup_system/migrations/0002_pickuprequest_location_pickuprequest_place_and_more.py new file mode 100644 index 00000000..4ddd96fd --- /dev/null +++ b/fiesta/apps/pickup_system/migrations/0002_pickuprequest_location_pickuprequest_place_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.7 on 2023-11-16 15:45 + +from django.db import migrations, models +import django.utils.timezone +import location_field.models.plain + + +class Migration(migrations.Migration): + + dependencies = [ + ('pickup_system', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='pickuprequest', + name='location', + field=location_field.models.plain.PlainLocationField(default='49.1922443,16.6113382', max_length=63), + ), + migrations.AddField( + model_name='pickuprequest', + name='place', + field=models.CharField(default='default', max_length=256), + preserve_default=False, + ), + migrations.AddField( + model_name='pickuprequest', + name='time', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='pickup time'), + preserve_default=False, + ), + migrations.AlterField( + model_name='pickupsystemconfiguration', + name='rolling_limit', + field=models.PositiveSmallIntegerField(default=0, editable=False), + ), + ] diff --git a/fiesta/apps/pickup_system/models/request.py b/fiesta/apps/pickup_system/models/request.py index 2c9b172c..2676f2bf 100644 --- a/fiesta/apps/pickup_system/models/request.py +++ b/fiesta/apps/pickup_system/models/request.py @@ -1,6 +1,9 @@ from __future__ import annotations +from django.db import models +from django.db.models import DateTimeField from django.utils.translation import gettext_lazy as _ +from location_field.models.plain import PlainLocationField from apps.fiestarequests.models import base_request_model_factory @@ -11,7 +14,17 @@ class PickupRequest(BaseRequestForPickupSystem): - # TODO: date/time/place + time = DateTimeField( + verbose_name=_("pickup time"), + ) + place = models.CharField( + max_length=256, + ) + location = PlainLocationField( + based_fields=["pickup_place"], + default="49.1922443,16.6113382", + zoom=4, + ) class Meta(BaseRequestForPickupSystem.Meta): verbose_name = _("pickup request") @@ -20,6 +33,11 @@ class Meta(BaseRequestForPickupSystem.Meta): def __str__(self): return f"Pickup Request {self.issuer}: {self.get_state_display()}" + @property + def location_as_google_maps_link(self): + return f"https://www.google.com/maps/place/{self.location}?zoom=15" + # return f"https://www.google.com/maps/search/?api=1&query={self.location}" + class PickupRequestMatch(BaseRequestMatchForPickupSystem): class Meta(BaseRequestForPickupSystem.Meta): diff --git a/fiesta/apps/pickup_system/templates/pickup_system/parts/request_match_card.html b/fiesta/apps/pickup_system/templates/pickup_system/parts/request_match_card.html index 892ee874..8ba95772 100644 --- a/fiesta/apps/pickup_system/templates/pickup_system/parts/request_match_card.html +++ b/fiesta/apps/pickup_system/templates/pickup_system/parts/request_match_card.html @@ -38,9 +38,35 @@

{{ br.note }}
- {#
#} - {# {% for interest in br.get_interests_display %}{{ interest }}{% endfor %}#} - {#
#} +
+ + + + + + + + + + + + + + + +
+ time ⏱️ + {{ br.time|date:"SHORT_DATETIME_FORMAT" }}
+ place πŸ“ + {{ br.place }}
+ location πŸ—ΊοΈ + + see on maps +
+
{% if br.state == br.State.MATCHED %} diff --git a/fiesta/apps/pickup_system/views/editor.py b/fiesta/apps/pickup_system/views/editor.py index f7591eec..a6673379 100644 --- a/fiesta/apps/pickup_system/views/editor.py +++ b/fiesta/apps/pickup_system/views/editor.py @@ -4,8 +4,8 @@ from django.urls import reverse, reverse_lazy from django.utils.translation import gettext_lazy as _ from django.views.generic import UpdateView -from django_tables2 import TemplateColumn -from django_tables2.columns.base import LinkTransform +from django_tables2 import TemplateColumn, tables +from django_tables2.columns.base import Column, LinkTransform from django_tables2.utils import Accessor from apps.fiestaforms.views.htmx import HtmxFormMixin @@ -27,11 +27,21 @@ class PickupRequestsTable(BaseRequestsTable): order_by="match", ) + time = tables.columns.DateTimeColumn() + + place = Column( + linkify=lambda record: record.location_as_google_maps_link, + ) + class Meta(BaseRequestsTable.Meta): fields = BaseRequestsTable.Meta.fields + ("match_request",) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + + if "matcher_picture" in self.columns: + self.columns["matcher_picture"].column.visible = False + if "issuer_name" in self.columns: # sometimes excluded self.columns["issuer_name"].link = LinkTransform( diff --git a/fiesta/fiesta/settings/project.py b/fiesta/fiesta/settings/project.py index 2bdec62a..ca46754f 100644 --- a/fiesta/fiesta/settings/project.py +++ b/fiesta/fiesta/settings/project.py @@ -93,6 +93,9 @@ def ALLOWED_HOSTS(self): "loginas", # editorjs integration "django_editorjs_fields", + # location fields + "location_field.apps.DefaultConfig", + # for trees "mptt", # health checks "health_check", @@ -122,3 +125,8 @@ def ALLOWED_HOSTS(self): # all EU countries first, then the rest COUNTRIES_FIRST = "AT BE BG HR CY CZ DK EE FI FR DE GR HU IE IT LV LT LU MT NL PL PT RO SK SI ES SE GB".split() COUNTRIES_FIRST_REPEAT = True + + LOCATION_FIELD = { + "map.provider": "openstreetmap", + "search.provider": "nominatim", + } diff --git a/poetry.lock b/poetry.lock index 0c6de7bb..14feb74f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -762,6 +762,17 @@ Django = ">=2.0" packaging = ">=21.0" urlman = ">=1.2.0" +[[package]] +name = "django-location-field" +version = "2.7.2" +description = "Location field for Django" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "django_location_field-2.7.2-py2.py3-none-any.whl", hash = "sha256:cdbb4b7d9f6ba9fb31e3bc2ec59492c77533f4fb5f3eeb7efd01eb3d4dbcf3b7"}, +] + [[package]] name = "django-loginas" version = "0.3.11" @@ -1954,4 +1965,4 @@ anyio = ">=3.0.0" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "15af9cfd990bad61791c1869168ecad5b40cef70d10d7917e0d47de2a6679057" +content-hash = "419cfd96bf6d442b1e827dcecd12d490afc81f290a308179246fca1224933f2a" diff --git a/pyproject.toml b/pyproject.toml index 64cf6140..be64c864 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ dj-database-url = "^2.0.0" django-health-check = "^3.17.0" django-money = "^3.2.0" gunicorn = "^21.2.0" +django-location-field = "^2.7.2" [tool.poetry.dev-dependencies] pre-commit = "^2.17.0" diff --git a/webpack/package.json b/webpack/package.json index 22dc0bd7..f07b7746 100644 --- a/webpack/package.json +++ b/webpack/package.json @@ -19,6 +19,7 @@ "css-loader": "^6.6.0", "daisyui": "^4.0.9", "htmx.org": "^1.9.6", + "jquery": "^3.7.1", "mini-css-extract-plugin": "^2.5.3", "postcss": "^8.4.31", "postcss-import": "^14.0.2", diff --git a/webpack/src/jquery.js b/webpack/src/jquery.js new file mode 100644 index 00000000..1afcdb02 --- /dev/null +++ b/webpack/src/jquery.js @@ -0,0 +1,3 @@ +import jquery from "jquery" + +window.jQuery = jquery diff --git a/webpack/webpack.base.config.js b/webpack/webpack.base.config.js index c9243926..bec21718 100644 --- a/webpack/webpack.base.config.js +++ b/webpack/webpack.base.config.js @@ -17,6 +17,9 @@ module.exports = { main: [ path.join(__dirname, './src/main.js') ], + jquery: [ + path.join(__dirname, './src/jquery.js') + ], }, output: { publicPath: PUBLIC_PATH, diff --git a/webpack/yarn.lock b/webpack/yarn.lock index 11a1cd06..a365086c 100644 --- a/webpack/yarn.lock +++ b/webpack/yarn.lock @@ -1845,6 +1845,11 @@ jiti@^1.18.2, jiti@^1.19.1: resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== +jquery@^3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.7.1.tgz#083ef98927c9a6a74d05a6af02806566d16274de" + integrity sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"