diff --git a/fiesta/apps/accounts/models/user.py b/fiesta/apps/accounts/models/user.py index b2c61d04..d617b21c 100644 --- a/fiesta/apps/accounts/models/user.py +++ b/fiesta/apps/accounts/models/user.py @@ -50,7 +50,7 @@ class Meta(AbstractUser.Meta): verbose_name_plural = _("users") # a few dynamic related models - buddy_system_matched_requests: models.QuerySet + buddy_system_request_matches: models.QuerySet profile: UserProfile diff --git a/fiesta/apps/buddy_system/admin.py b/fiesta/apps/buddy_system/admin.py index aee4903e..20f16846 100644 --- a/fiesta/apps/buddy_system/admin.py +++ b/fiesta/apps/buddy_system/admin.py @@ -2,9 +2,9 @@ from django.contrib import admin -from ..fiestarequests.admin import BaseRequestAdmin +from ..fiestarequests.admin import BaseRequestAdmin, BaseRequestMatchAdmin from ..plugins.admin import BaseChildConfigurationAdmin -from .models import BuddyRequest, BuddySystemConfiguration +from .models import BuddyRequest, BuddyRequestMatch, BuddySystemConfiguration @admin.register(BuddySystemConfiguration) @@ -15,3 +15,8 @@ class BuddySystemConfigurationAdmin(BaseChildConfigurationAdmin): @admin.register(BuddyRequest) class BuddyRequestAdmin(BaseRequestAdmin): pass + + +@admin.register(BuddyRequestMatch) +class BuddyRequestMatchAdmin(BaseRequestMatchAdmin): + pass diff --git a/fiesta/apps/buddy_system/forms.py b/fiesta/apps/buddy_system/forms.py index c0080dd1..445c407c 100644 --- a/fiesta/apps/buddy_system/forms.py +++ b/fiesta/apps/buddy_system/forms.py @@ -6,9 +6,8 @@ from django.utils.translation import gettext_lazy as _ from apps.accounts.models import UserProfile -from apps.buddy_system.models import BuddyRequest +from apps.buddy_system.models import BuddyRequest, BuddyRequestMatch from apps.fiestaforms.fields.array import ChoicedArrayField -from apps.fiestaforms.fields.datetime import DateTimeLocalField from apps.fiestaforms.forms import BaseModelForm from apps.fiestaforms.widgets.models import ActiveLocalMembersFromSectionWidget, UserWidget @@ -29,7 +28,7 @@ class NewBuddyRequestForm(BaseModelForm): class Meta: model = BuddyRequest fields = ( - "issuer_note", + "note", "interests", "responsible_section", "issuer", @@ -42,12 +41,12 @@ class Meta: "issuer": HiddenInput, } labels = { - "issuer_note": _("Tell us about yourself"), + "note": _("Tell us about yourself"), "interests": _("What are you into?"), } help_texts = { - "issuer_note": lazy( - lambda: render_to_string("buddy_system/parts/buddy_request_issuer_note_help.html"), + "note": lazy( + lambda: render_to_string("buddy_system/parts/buddy_request_note_help.html"), str, ) } @@ -65,9 +64,9 @@ def __init__(self, *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["issuer_note"].disabled = True + # self.fields["matched_by"].disabled = True + # self.fields["matched_at"].disabled = True + self.fields["note"].disabled = True self.fields["interests"].disabled = True class Meta: @@ -75,18 +74,18 @@ class Meta: fields = ( "issuer", "state", - "issuer_note", + "note", "interests", - "matched_by", - "matched_at", + # "matched_by", + # "matched_at", ) field_classes = { "interests": ChoicedArrayField, - "matched_at": DateTimeLocalField, + # "matched_at": DateTimeLocalField, } widgets = { "issuer": UserWidget, - "matched_by": ActiveLocalMembersFromSectionWidget, + # "matched_by": ActiveLocalMembersFromSectionWidget, } @@ -96,18 +95,11 @@ class QuickBuddyMatchForm(BaseModelForm): 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["issuer"].disabled = True class Meta: - model = BuddyRequest - fields = ( - "issuer", - "matched_by", - ) + model = BuddyRequestMatch + fields = ("matcher",) widgets = { - "issuer": UserWidget, - "matched_by": ActiveLocalMembersFromSectionWidget, + "matcher": ActiveLocalMembersFromSectionWidget, } diff --git a/fiesta/apps/buddy_system/migrations/0021_remove_buddyrequest_issuer_note_and_more.py b/fiesta/apps/buddy_system/migrations/0021_remove_buddyrequest_issuer_note_and_more.py new file mode 100644 index 00000000..e7ec4a4e --- /dev/null +++ b/fiesta/apps/buddy_system/migrations/0021_remove_buddyrequest_issuer_note_and_more.py @@ -0,0 +1,59 @@ +# Generated by Django 4.2.4 on 2023-09-24 19:17 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django_extensions.db.fields +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('buddy_system', '0020_rename_description_buddyrequest_issuer_note'), + ] + + operations = [ + migrations.RemoveField( + model_name='buddyrequest', + name='issuer_note', + ), + migrations.RemoveField( + model_name='buddyrequest', + name='matched_at', + ), + migrations.RemoveField( + model_name='buddyrequest', + name='matched_by', + ), + migrations.AddField( + model_name='buddyrequest', + name='note', + field=models.TextField(default='', verbose_name='text from issuer'), + preserve_default=False, + ), + migrations.AddField( + model_name='buddysystemconfiguration', + name='enable_note_from_matcher', + field=models.BooleanField(default=True, help_text='Allows matcher to reply with custom notes to the request issuer'), + ), + migrations.CreateModel( + name='BuddyRequestMatch', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, 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='buddy_system_request_matches', to=settings.AUTH_USER_MODEL, verbose_name='matched by')), + ('request', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='match', to='buddy_system.buddyrequest', verbose_name='request')), + ], + options={ + 'verbose_name': 'buddy request match', + 'verbose_name_plural': 'buddy request matches', + 'ordering': ('-created',), + 'get_latest_by': 'modified', + 'abstract': False, + }, + ), + ] diff --git a/fiesta/apps/buddy_system/models/__init__.py b/fiesta/apps/buddy_system/models/__init__.py index 6f575a1c..e05998e1 100644 --- a/fiesta/apps/buddy_system/models/__init__.py +++ b/fiesta/apps/buddy_system/models/__init__.py @@ -1,9 +1,10 @@ from __future__ import annotations from .configuration import BuddySystemConfiguration -from .request import BuddyRequest +from .request import BuddyRequest, BuddyRequestMatch __all__ = [ "BuddySystemConfiguration", "BuddyRequest", + "BuddyRequestMatch", ] diff --git a/fiesta/apps/buddy_system/models/request.py b/fiesta/apps/buddy_system/models/request.py index c76e97b0..014a9f4c 100644 --- a/fiesta/apps/buddy_system/models/request.py +++ b/fiesta/apps/buddy_system/models/request.py @@ -7,7 +7,10 @@ from apps.fiestarequests.models import base_request_model_factory from apps.utils.models.fields import ArrayFieldWithDisplayableChoices -BaseRequestForBuddySystem = base_request_model_factory(related_base="buddy_system") +BaseRequestForBuddySystem, BaseRequestMatchForBuddySystem = base_request_model_factory( + final_request_model_name="buddy_system.BuddyRequest", + related_base="buddy_system", +) class BuddyRequest(BaseRequestForBuddySystem): @@ -29,3 +32,12 @@ class Meta(BaseRequestForBuddySystem.Meta): def __str__(self): return f"Buddy Request {self.issuer}: {self.get_state_display()}" + + +class BuddyRequestMatch(BaseRequestMatchForBuddySystem): + class Meta(BaseRequestForBuddySystem.Meta): + verbose_name = _("buddy request match") + verbose_name_plural = _("buddy request matches") + + def __str__(self): + return f"Buddy Request Match {self.matcher}: {self.request}" 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 f6e53044..0bf19511 100644 --- a/fiesta/apps/buddy_system/templates/buddy_system/dashboard_block.html +++ b/fiesta/apps/buddy_system/templates/buddy_system/dashboard_block.html @@ -39,15 +39,15 @@
It's a match!
- You have been matched with {{ br.matched_by }}. + You have been matched with {{ br.match.matcher }}.
{% endif %} {% else %} {% get_waiting_requests_to_match as waiting_brs %}
{% with waiting_brs.count as count %} - {{ count }} waiting request - {{ count|pluralize:"s" }} + {{ count }} + waiting request{{ count|pluralize:"s" }} {% endwith %}
diff --git a/fiesta/apps/buddy_system/templates/buddy_system/index_international.html b/fiesta/apps/buddy_system/templates/buddy_system/index_international.html index 3200a51b..768d9625 100644 --- a/fiesta/apps/buddy_system/templates/buddy_system/index_international.html +++ b/fiesta/apps/buddy_system/templates/buddy_system/index_international.html @@ -42,7 +42,7 @@

{% endif %}

-
{{ br.issuer_note }}
+
{{ br.note }}
diff --git a/fiesta/apps/buddy_system/templatetags/buddy_system.py b/fiesta/apps/buddy_system/templatetags/buddy_system.py index e2668d9c..78171398 100644 --- a/fiesta/apps/buddy_system/templatetags/buddy_system.py +++ b/fiesta/apps/buddy_system/templatetags/buddy_system.py @@ -76,10 +76,10 @@ def get_matched_buddy_requests(context): request: HttpRequest = context["request"] # TODO: limit by semester / time - return request.user.buddy_system_matched_requests.filter( - responsible_section=request.membership.section, - state=BuddyRequest.State.MATCHED, - ).order_by("-matched_at") + return request.user.buddy_system_request_matches.filter( + request__responsible_section=request.membership.section, + request__state=BuddyRequest.State.MATCHED, + ).order_by("-created") @register.filter diff --git a/fiesta/apps/buddy_system/views/editor.py b/fiesta/apps/buddy_system/views/editor.py index b2750dd5..589c64cf 100644 --- a/fiesta/apps/buddy_system/views/editor.py +++ b/fiesta/apps/buddy_system/views/editor.py @@ -3,15 +3,16 @@ from django.contrib.messages.views import SuccessMessageMixin from django.contrib.postgres.search import SearchVector from django.forms import TextInput +from django.shortcuts import get_object_or_404 from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ -from django.views.generic import UpdateView +from django.views.generic import CreateView, UpdateView from django_filters import CharFilter, ChoiceFilter, ModelChoiceFilter from django_tables2 import Column, TemplateColumn, tables from django_tables2.utils import Accessor from apps.buddy_system.forms import BuddyRequestEditorForm, QuickBuddyMatchForm -from apps.buddy_system.models import BuddyRequest +from apps.buddy_system.models import BuddyRequest, BuddyRequestMatch from apps.fiestaforms.views.htmx import HtmxFormMixin from apps.fiestatables.columns import ImageColumn, NaturalDatetimeColumn from apps.fiestatables.filters import BaseFilterSet, ProperDateFromToRangeFilter @@ -153,17 +154,20 @@ class QuickBuddyMatchView( SuccessMessageMixin, HtmxFormMixin, AjaxViewMixin, - UpdateView, + CreateView, ): template_name = "buddy_system/editor/quick_match.html" ajax_template_name = "buddy_system/editor/quick_match_form.html" - model = BuddyRequest + model = BuddyRequestMatch form_class = QuickBuddyMatchForm success_url = reverse_lazy("buddy_system:requests") success_message = _("Buddy request has been matched.") def form_valid(self, form): - form.instance.state = BuddyRequest.State.MATCHED - form.instance.save(update_fields=["state"]) + request = get_object_or_404(BuddyRequest, pk=self.kwargs["pk"]) + form.instance.request = request + request.state = BuddyRequest.State.MATCHED + request.save(update_fields=["state"]) + return super().form_valid(form) diff --git a/fiesta/apps/buddy_system/views/matching.py b/fiesta/apps/buddy_system/views/matching.py index 673af6df..48f659ee 100644 --- a/fiesta/apps/buddy_system/views/matching.py +++ b/fiesta/apps/buddy_system/views/matching.py @@ -9,7 +9,7 @@ from django.views.generic.detail import BaseDetailView from django_htmx.http import HttpResponseClientRedirect -from apps.buddy_system.models import BuddyRequest, BuddySystemConfiguration +from apps.buddy_system.models import BuddyRequest, BuddyRequestMatch, BuddySystemConfiguration from apps.files.views import NamespacedFilesServeView from apps.plugins.middleware.plugin import HttpRequest from apps.plugins.views import PluginConfigurationViewMixin @@ -56,11 +56,21 @@ def get_queryset(self): ) def post(self, request, pk: uuid.UUID): - BuddyRequest.objects.match_by( - request=self.get_object(), + br: BuddyRequest = self.get_object() + + match = BuddyRequestMatch( + request=br, matcher=self.request.user, + # TODO: better + note=self.request.POST.get("note"), ) + # TODO: check matcher relation to responsible section? + match.save() + + br.match = match + br.state = BuddyRequest.State.MATCHED + messages.success(request, _("Request successfully matched!")) # TODO: target URL? return HttpResponseClientRedirect("/") diff --git a/fiesta/apps/fiestarequests/admin.py b/fiesta/apps/fiestarequests/admin.py index 922dfe83..a397f115 100644 --- a/fiesta/apps/fiestarequests/admin.py +++ b/fiesta/apps/fiestarequests/admin.py @@ -5,7 +5,9 @@ class BaseRequestAdmin(ModelAdmin): - list_display = ["responsible_section", "issuer", "state", "matched_by", "matched_at", "created"] + # https://github.com/gitaarik/django-admin-relation-links + + list_display = ["responsible_section", "issuer", "state", "match", "created"] date_hierarchy = "created" @@ -15,7 +17,7 @@ class BaseRequestAdmin(ModelAdmin): "state", ] - autocomplete_fields = ["issuer", "matched_by"] + autocomplete_fields = ["issuer"] search_fields = [ "issuer__username", @@ -24,3 +26,28 @@ class BaseRequestAdmin(ModelAdmin): "issuer__first_name", "responsible_section__name", ] + + +class BaseRequestMatchAdmin(ModelAdmin): + list_display = ["matcher", "note", "created"] + + date_hierarchy = "created" + + list_filter = [ + ("request__responsible_section", admin.RelatedOnlyFieldListFilter), + ("request__responsible_section__country", admin.AllValuesFieldListFilter), + ] + + autocomplete_fields = ["matcher"] + + search_fields = [ + "request__issuer__username", + "request__issuer__email", + "request__issuer__last_name", + "request__issuer__first_name", + "matcher__username", + "matcher__email", + "matcher__last_name", + "matcher__first_name", + "request_match__responsible_section__name", + ] diff --git a/fiesta/apps/fiestarequests/matching_policy.py b/fiesta/apps/fiestarequests/matching_policy.py index d8ab8198..e4916c38 100644 --- a/fiesta/apps/fiestarequests/matching_policy.py +++ b/fiesta/apps/fiestarequests/matching_policy.py @@ -39,7 +39,7 @@ def _base_filter(cls, membership: SectionMembership) -> Q: return Q( responsible_section=membership.section, state=BuddyRequest.State.CREATED, - matched_by=None, # to be sure + match__isnull=True, # to be sure ) diff --git a/fiesta/apps/fiestarequests/models/managers/request.py b/fiesta/apps/fiestarequests/models/managers/request.py index fbfc95af..e7c544db 100644 --- a/fiesta/apps/fiestarequests/models/managers/request.py +++ b/fiesta/apps/fiestarequests/models/managers/request.py @@ -6,17 +6,8 @@ from django.db.models import Manager if typing.TYPE_CHECKING: - from apps.accounts.models import User from apps.fiestarequests.models.request import BaseRequestProtocol class BaseRequestManager(Manager): model: BaseRequestProtocol | models.Model - - def match_by(self, request: BaseRequestProtocol | models.Model, matcher: User): - request.matched_by = matcher - request.state = self.model.State.MATCHED - - # TODO: check matcher relation to responsible section? - - request.save(update_fields=["matched_by", "matched_at", "state"]) diff --git a/fiesta/apps/fiestarequests/models/request.py b/fiesta/apps/fiestarequests/models/request.py index 60ac1d35..f0afca60 100644 --- a/fiesta/apps/fiestarequests/models/request.py +++ b/fiesta/apps/fiestarequests/models/request.py @@ -5,9 +5,8 @@ from django.db import models from django.db.models import TextChoices -from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ -from django_lifecycle import BEFORE_SAVE, LifecycleModelMixin, hook +from django_lifecycle import LifecycleModelMixin from apps.accounts.models import User from apps.fiestarequests.models.managers.request import BaseRequestManager @@ -32,7 +31,10 @@ class State(TextChoices): objects: BaseRequestManager -def base_request_model_factory(related_base: str): +def base_request_model_factory( + related_base: str, + final_request_model_name: str, +): """ Creates a base model for requests-like models. """ @@ -66,31 +68,34 @@ class Meta(BaseTimestampedModel.Meta): verbose_name=_("responsible section"), db_index=True, ) - matched_by = models.ForeignKey( + + note = models.TextField( + verbose_name=_("text from issuer"), + ) + + class BaseRequestMatch(BaseTimestampedModel): + class Meta(BaseTimestampedModel.Meta): + abstract = True + ordering = ("-created",) + + request = models.OneToOneField( + final_request_model_name, + related_name="match", + on_delete=models.CASCADE, + verbose_name=_("request"), + ) + + matcher = models.ForeignKey( "accounts.User", - related_name=f"{related_base}_matched_requests", + related_name=f"{related_base}_request_matches", on_delete=models.RESTRICT, verbose_name=_("matched by"), db_index=True, - null=True, - blank=True, - ) - matched_at = models.DateTimeField( - verbose_name=_("matched at"), - null=True, - blank=True, ) - issuer_note = models.TextField( - verbose_name=_("text from issuer"), - ) - - matcher_note = models.TextField( + note = models.TextField( verbose_name=_("text from matcher"), + blank=True, ) - @hook(BEFORE_SAVE, when="matched_by", was=None, is_not=None) - def set_matched_at(self): - self.matched_at = now() - - return BaseRequest + return BaseRequest, BaseRequestMatch