- {% for br in matched_requests|slice:":3" %}
- {% get_user_picture br.issuer as buddy_picture %}
+ {% for rm in request_matches|slice:":3" %}
+ {% get_user_picture rm.request.issuer as buddy_picture %}
{% if buddy_picture %}
-
+
{% endif %}
{% endfor %}
- {% if matched_requests|length > 3 %}
+ {% if request_matches|length > 3 %}
{% endif %}
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 dd2f43d0..54d719de 100644
--- a/fiesta/apps/buddy_system/templates/buddy_system/index_international.html
+++ b/fiesta/apps/buddy_system/templates/buddy_system/index_international.html
@@ -1,4 +1,7 @@
{% extends "fiesta/base.html" %}
+{% load buddy_system %}
+{% load user_profile %}
+{% load utils %}
{% load i18n %}
{% load breadcrumbs %}
@@ -9,24 +12,15 @@
{% endblock upper_head %}
{% block main %}
-
+
+ {% for br in requests %}
+ {% blocktranslate with created=br.created|date asvar title %}Your Request from {{ created }}{% endblocktranslate %}
+ {% include "buddy_system/parts/request_match_card.html" with br=br title=title connect_with=br.match.matcher %}
+ {% empty %}
+
+ {% endfor %}
{% endblock %}
diff --git a/fiesta/apps/buddy_system/templates/buddy_system/matching_requests.html b/fiesta/apps/buddy_system/templates/buddy_system/matching_requests.html
index 211768cd..edfb4763 100644
--- a/fiesta/apps/buddy_system/templates/buddy_system/matching_requests.html
+++ b/fiesta/apps/buddy_system/templates/buddy_system/matching_requests.html
@@ -60,7 +60,7 @@
{% endif %}
{#
are added by filter #}
- {{ br.description|censor_description|linebreaks }}
+ {{ br.note|censor_description|linebreaks }}
{% if br.interests %}
@@ -78,10 +78,13 @@
x-ref="dialog">
diff --git a/fiesta/apps/buddy_system/templates/buddy_system/my_buddies.html b/fiesta/apps/buddy_system/templates/buddy_system/my_buddies.html
index 4e68d760..fa39053f 100644
--- a/fiesta/apps/buddy_system/templates/buddy_system/my_buddies.html
+++ b/fiesta/apps/buddy_system/templates/buddy_system/my_buddies.html
@@ -12,77 +12,7 @@
{% endblock upper_head %}
{% block main %}
-
- {% for br in object_list.all %}
-
-
-
- {{ br.issuer.full_name_official }}
-
- {{ br.matched_at|date }}
-
-
-
-
- {#
are added by filter #}
- {{ br.description|censor_description|linebreaks }}
-
-
-
- {% for interest in br.get_interests_display %}
- {{ interest }}
- {% endfor %}
-
-
-
- {% if br.issuer.profile.picture %}
-
-
-
-
- {% if br.issuer.profile_or_none.facebook %}
-
-
-
-
-
- {% endif %}
- {% if br.issuer.profile_or_none.instagram %}
-
-
-
-
-
- {% endif %}
- {% if br.issuer.email %}
-
-
@
- {% endif %}
-
-
- {% endif %}
-
-
- {% endfor %}
-
-
+ {% for match in object_list.all %}
+ {% include "buddy_system/parts/request_match_card.html" with br=match.request title=match.request.issuer.full_name connect_with=match.request.issuer %}
+ {% endfor %}
{% endblock %}
diff --git a/fiesta/apps/buddy_system/templates/buddy_system/parts/buddy_request_description_help.html b/fiesta/apps/buddy_system/templates/buddy_system/parts/buddy_request_description_help.html
deleted file mode 100644
index a3c30daf..00000000
--- a/fiesta/apps/buddy_system/templates/buddy_system/parts/buddy_request_description_help.html
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
Few tips to write about yourself:
-
-
- Do you have any Erasmus dreams ? Mention them!
-
- You have one special passion? Tell us about it!
-
- You prefer talkative and active buddy with dayily contact? Write it down.
-
- Or you're not sure about something and just wanna a few hints ? No problem!
-
- Please, do not include any contact details, neither name nor gender -- age is fine.
-
-
diff --git a/fiesta/apps/buddy_system/templates/buddy_system/parts/buddy_request_note_help.html b/fiesta/apps/buddy_system/templates/buddy_system/parts/buddy_request_note_help.html
new file mode 100644
index 00000000..fdb8d30e
--- /dev/null
+++ b/fiesta/apps/buddy_system/templates/buddy_system/parts/buddy_request_note_help.html
@@ -0,0 +1,17 @@
+
+
Few tips to write about yourself:
+
+
+ Do you have any Erasmus dreams ? Mention them!
+
+
+ You have one special passion? Tell us about it!
+
+ You prefer talkative and active buddy with daily contact? Write it down.
+
+ Or you're not sure about something and just wanna a few hints ? No problem!
+
+
+ Please, do not include any contact details, neither name nor gender -- age is fine.
+
+
diff --git a/fiesta/apps/buddy_system/templates/buddy_system/parts/request_match_card.html b/fiesta/apps/buddy_system/templates/buddy_system/parts/request_match_card.html
new file mode 100644
index 00000000..6af1c4ab
--- /dev/null
+++ b/fiesta/apps/buddy_system/templates/buddy_system/parts/request_match_card.html
@@ -0,0 +1,178 @@
+{% load i18n %}
+{% load static %}
+{% load utils %}
+{% load user_profile %}
+{% load buddy_system %}
+
+
+
+ {{ title }}
+
+
+ {{ br.get_state_display }}
+
+
+
+
+
+
+
+
+ {% get_user_picture br.issuer as issuer_picture %}
+
+ {% if issuer_picture %}
+
+ {% else %}
+
{{ br.issuer.first_name|first }}{{ br.issuer.last_name|first }}
+ {% endif %}
+
+
+
{{ br.note }}
+
+ {% for interest in br.issuer.profile.get_interests_display %}
+ {{ interest }}
+ {% endfor %}
+
+
+
+ {% if br.state == br.State.MATCHED %}
+
+
+
+
+ {% get_user_picture br.match.matcher as matcher_picture %}
+
+ {% if 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 }}
+
+
+
+ {% elif br.state == br.State.CREATED %}
+
+
+
+
+ {% get_waiting_buddy_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/buddy_system/templates/buddy_system/parts/requests_editor_match_btn.html b/fiesta/apps/buddy_system/templates/buddy_system/parts/requests_editor_match_btn.html
index b5236d02..369f4f2b 100644
--- a/fiesta/apps/buddy_system/templates/buddy_system/parts/requests_editor_match_btn.html
+++ b/fiesta/apps/buddy_system/templates/buddy_system/parts/requests_editor_match_btn.html
@@ -1,7 +1,11 @@
{% load i18n %}
-{% if not record.matched_by %}
-
{% trans "Match" %}
-{% endif %}
+
+ {% if record.match %}
+ {% trans "Change buddy" %}
+ {% else %}
+ {% trans "Match" %}
+ {% endif %}
+
diff --git a/fiesta/apps/buddy_system/templatetags/buddy_system.py b/fiesta/apps/buddy_system/templatetags/buddy_system.py
index e2668d9c..494d73c2 100644
--- a/fiesta/apps/buddy_system/templatetags/buddy_system.py
+++ b/fiesta/apps/buddy_system/templatetags/buddy_system.py
@@ -1,6 +1,5 @@
from __future__ import annotations
-import hashlib
import re
from django import template
@@ -10,7 +9,6 @@
from apps.plugins.middleware.plugin import HttpRequest
from apps.plugins.models import Plugin
from apps.plugins.utils import all_plugins_mapped_to_class
-from apps.utils.models.query import get_single_object_or_none
register = template.Library()
@@ -38,11 +36,12 @@ def censor_description(description: str) -> str:
def get_current_buddy_request_of_user(context):
request: HttpRequest = context["request"]
- return get_single_object_or_none(
- request.membership.user.buddy_system_issued_requests.filter(
+ try:
+ return request.membership.user.buddy_system_issued_requests.filter(
responsible_section=request.membership.section,
- )
- ) # TODO: could be more then one?
+ ).latest("created")
+ except BuddyRequest.DoesNotExist:
+ return None
@register.simple_tag(takes_context=True)
@@ -76,24 +75,16 @@ 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
-def get_color_by_text(name: str) -> str:
- hash_object = hashlib.md5(name.encode(), usedforsecurity=False)
- hash_hex = hash_object.hexdigest()
-
- r = int(hash_hex[0:2], 16)
- g = int(hash_hex[2:4], 16)
- b = int(hash_hex[4:6], 16)
-
- if r + g + b < 100:
- r += 30
- g += 30
- b += 30
-
- return f"rgb({r}, {g}, {b})"
+def request_state_to_css_variant(state: BuddyRequest.State):
+ return {
+ BuddyRequest.State.CREATED: "info",
+ BuddyRequest.State.MATCHED: "success",
+ BuddyRequest.State.CANCELLED: "danger",
+ }.get(state)
diff --git a/fiesta/apps/buddy_system/urls.py b/fiesta/apps/buddy_system/urls.py
index 44f50b16..61202641 100644
--- a/fiesta/apps/buddy_system/urls.py
+++ b/fiesta/apps/buddy_system/urls.py
@@ -6,7 +6,7 @@
from .views import BuddySystemIndexView
from .views.editor import BuddyRequestEditorDetailView, BuddyRequestsEditorView, QuickBuddyMatchView
from .views.matches import MyBuddies
-from .views.matching import MatchingRequestsView, ProfilePictureServeView, TakeBuddyRequestView
+from .views.matching import IssuerPictureServeView, MatcherPictureServeView, MatchingRequestsView, TakeBuddyRequestView
from .views.request import BuddySystemEntrance, NewRequestView, SignUpBeforeEntranceView, WannaBuddyView
urlpatterns = [
@@ -30,5 +30,6 @@
path("detail/
", BuddyRequestEditorDetailView.as_view(), name="editor-detail"),
path("quick-match/", QuickBuddyMatchView.as_view(), name="quick-match"),
# serve profile picture with proxy view
- ProfilePictureServeView.as_url(user_profile_picture_storage, url_name="serve-issuer-profile-picture"),
+ 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/buddy_system/views/editor.py b/fiesta/apps/buddy_system/views/editor.py
index b2750dd5..b3bdaddd 100644
--- a/fiesta/apps/buddy_system/views/editor.py
+++ b/fiesta/apps/buddy_system/views/editor.py
@@ -2,6 +2,7 @@
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.utils.translation import gettext_lazy as _
@@ -11,7 +12,7 @@
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
@@ -34,12 +35,14 @@ class RequestFilter(BaseFilterSet):
widget=TextInput(attrs={"placeholder": _("Hannah, Diego, Joe...")}),
)
state = ChoiceFilter(choices=BuddyRequest.State.choices)
- matched_when = ProperDateFromToRangeFilter(field_name="matched_at")
+ matched_when = ProperDateFromToRangeFilter(
+ field_name="match__created",
+ )
- matched_by_faculty = ModelChoiceFilter(
+ matcher_faculty = ModelChoiceFilter(
queryset=related_faculties,
label=_("Faculty of matcher"),
- field_name="matched_by__profile__home_faculty",
+ field_name="match__matcher__profile__home_faculty",
)
def filter_search(self, queryset, name, value):
@@ -47,8 +50,8 @@ def filter_search(self, queryset, name, value):
search=SearchVector(
"issuer__last_name",
"issuer__first_name",
- "matched_by__last_name",
- "matched_by__first_name",
+ "match__matcher__last_name",
+ "match__matcher__first_name",
"state",
)
).filter(search=value)
@@ -62,36 +65,41 @@ class BuddyRequestsTable(tables.Table):
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=_("Request from"),
+ verbose_name=_("Issuer"),
)
issuer__profile__picture = ImageColumn(verbose_name="🧑")
- matched_by_name = Column(
- accessor="matched_by.full_name_official",
+ matcher_name = Column(
+ accessor="match.matcher.full_name_official",
order_by=(
- "matched_by__last_name",
- "matched_by__first_name",
- "matched_by__username",
+ "match__matcher__last_name",
+ "match__matcher__first_name",
+ "match__matcher__username",
),
)
- matched_by_email = Column(
- accessor="matched_by.email",
+ matcher_email = Column(
+ accessor="match.matcher.email",
visible=False,
)
- matched_by_picture = ImageColumn(
- accessor="matched_by.profile.picture",
- verbose_name=_("Buddy"),
+ matcher_picture = ImageColumn(
+ accessor="match.matcher.profile.picture",
+ verbose_name=_("Matcher"),
)
match_request = TemplateColumn(
template_name="buddy_system/parts/requests_editor_match_btn.html",
exclude_from_export=True,
- order_by="matched_at",
+ order_by="match",
)
- matched_at = NaturalDatetimeColumn()
+ 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
@@ -101,9 +109,10 @@ class Meta:
"issuer__full_name_official",
"issuer__profile__picture",
"state",
- "matched_by_name",
- "matched_by_picture",
- "matched_at",
+ "matcher_name",
+ "matcher_picture",
+ "requested",
+ "matched",
"match_request",
"...",
)
@@ -163,7 +172,30 @@ class QuickBuddyMatchView(
success_url = reverse_lazy("buddy_system:requests")
success_message = _("Buddy request has been matched.")
+ def get_initial(self):
+ try:
+ return {
+ "matcher": self.get_object().match.matcher,
+ }
+ except BuddyRequestMatch.DoesNotExist:
+ return {}
+
+ @transaction.atomic
def form_valid(self, form):
- form.instance.state = BuddyRequest.State.MATCHED
- form.instance.save(update_fields=["state"])
+ br: BuddyRequest = self.get_object()
+
+ if br.match:
+ # could be already matched by someone else
+ br.match.delete()
+
+ match = BuddyRequestMatch(
+ request=br,
+ matcher=form.cleaned_data.get("matcher"),
+ )
+
+ match.save()
+
+ br.state = BuddyRequest.State.MATCHED
+ br.save(update_fields=["state"])
+
return super().form_valid(form)
diff --git a/fiesta/apps/buddy_system/views/index.py b/fiesta/apps/buddy_system/views/index.py
index f5514407..4eb324b2 100644
--- a/fiesta/apps/buddy_system/views/index.py
+++ b/fiesta/apps/buddy_system/views/index.py
@@ -2,7 +2,7 @@
from django.views.generic import TemplateView
-from apps.buddy_system.models import BuddySystemConfiguration
+from apps.buddy_system.models import BuddyRequest, BuddySystemConfiguration
from apps.plugins.views import PluginConfigurationViewMixin
from apps.sections.middleware.user_membership import HttpRequest
from apps.sections.models import SectionMembership
@@ -16,6 +16,10 @@ class BuddySystemIndexView(
):
request: HttpRequest
+ extra_context = {
+ "RequestState": BuddyRequest.State,
+ }
+
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
diff --git a/fiesta/apps/buddy_system/views/matches.py b/fiesta/apps/buddy_system/views/matches.py
index 96d3f82b..ab4d1cb2 100644
--- a/fiesta/apps/buddy_system/views/matches.py
+++ b/fiesta/apps/buddy_system/views/matches.py
@@ -12,4 +12,6 @@ class MyBuddies(EnsureLocalUserViewMixin, ListView):
template_name = "buddy_system/my_buddies.html"
def get_queryset(self):
- return self.request.user.buddy_system_matched_requests.prefetch_related("issuer__profile")
+ return self.request.user.buddy_system_request_matches.prefetch_related(
+ "request__issuer__profile"
+ ).select_related("request", "matcher")
diff --git a/fiesta/apps/buddy_system/views/matching.py b/fiesta/apps/buddy_system/views/matching.py
index 4785d3b4..035d1d17 100644
--- a/fiesta/apps/buddy_system/views/matching.py
+++ b/fiesta/apps/buddy_system/views/matching.py
@@ -4,12 +4,13 @@
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, 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
@@ -55,30 +56,68 @@ def get_queryset(self):
membership=self.request.membership,
)
+ @transaction.atomic
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,
+ note=self.request.POST.get("note"),
)
+ # 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 ProfilePictureServeView(
+class IssuerPictureServeView(
PluginConfigurationViewMixin[BuddySystemConfiguration],
NamespacedFilesServeView,
):
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(
- Q(issuer__profile__picture=name) | Q(matched_by__profile__picture=name)
+ issuer__profile__picture=name,
)
# does have the section enabled picture displaying?
return (related_requests.exists() and self.configuration and self.configuration.display_issuer_picture) or (
related_requests.filter(
- Q(matched_by=request.user) | Q(issuer=request.user), state=BuddyRequest.State.MATCHED
- ).exists()
+ state=BuddyRequest.State.MATCHED,
+ )
+ .filter(
+ Q(match__matcher=request.user) | Q(issuer=request.user),
+ )
+ .exists()
+ )
+
+
+class MatcherPictureServeView(
+ PluginConfigurationViewMixin[BuddySystemConfiguration],
+ NamespacedFilesServeView,
+):
+ 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/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/configuration.py b/fiesta/apps/fiestarequests/models/configuration.py
index ea070a06..72842fa4 100644
--- a/fiesta/apps/fiestarequests/models/configuration.py
+++ b/fiesta/apps/fiestarequests/models/configuration.py
@@ -1,6 +1,7 @@
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.plugins.models import BasePluginConfiguration
@@ -23,6 +24,11 @@ class BaseRequestSystemConfiguration(BasePluginConfiguration):
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?
diff --git a/fiesta/apps/fiestarequests/models/managers/request.py b/fiesta/apps/fiestarequests/models/managers/request.py
index fbfc95af..37220434 100644
--- a/fiesta/apps/fiestarequests/models/managers/request.py
+++ b/fiesta/apps/fiestarequests/models/managers/request.py
@@ -6,17 +6,12 @@
from django.db.models import Manager
if typing.TYPE_CHECKING:
- from apps.accounts.models import User
- from apps.fiestarequests.models.request import BaseRequestProtocol
+ from apps.fiestarequests.models.request import BaseRequestMatchProtocol, 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"])
+class BaseRequestMatchManager(Manager):
+ model: BaseRequestMatchProtocol | models.Model
diff --git a/fiesta/apps/fiestarequests/models/request.py b/fiesta/apps/fiestarequests/models/request.py
index 0ace4ac7..0e395ba1 100644
--- a/fiesta/apps/fiestarequests/models/request.py
+++ b/fiesta/apps/fiestarequests/models/request.py
@@ -1,16 +1,14 @@
from __future__ import annotations
-import datetime
import typing
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
+from apps.fiestarequests.models.managers.request import BaseRequestManager, BaseRequestMatchManager
from apps.sections.models import Section
from apps.utils.models import BaseTimestampedModel
@@ -25,14 +23,27 @@ class State(TextChoices):
state: models.CharField | State
issuer: models.ForeignKey | User
responsible_section: models.ForeignKey | Section
- matched_by: models.ForeignKey | User
- matched_at: models.DateTimeField | datetime.datetime
- description: models.TextField | str
+ note: models.TextField | str
objects: BaseRequestManager
-def base_request_model_factory(related_base: str):
+class BaseRequestMatchProtocol(typing.Protocol):
+ request: models.ForeignKey | BaseRequestProtocol
+ matcher: models.ForeignKey | User
+ note: models.TextField | str
+
+ objects: BaseRequestMatchManager
+
+
+def base_request_model_factory(
+ related_base: str,
+ final_request_model_name: str,
+):
+ """
+ Creates a base model for requests-like models.
+ """
+
class BaseRequest(LifecycleModelMixin, BaseTimestampedModel):
class Meta(BaseTimestampedModel.Meta):
abstract = True
@@ -44,8 +55,8 @@ class Meta(BaseTimestampedModel.Meta):
state = models.CharField(
verbose_name=_("state"),
- choices=BaseRequestProtocol.State.choices,
- default=BaseRequestProtocol.State.CREATED,
+ choices=State.choices,
+ default=State.CREATED,
max_length=16,
)
issuer = models.ForeignKey(
@@ -62,27 +73,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,
)
- description = models.TextField(
- verbose_name=_("description"),
+ 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
diff --git a/fiesta/apps/files/views.py b/fiesta/apps/files/views.py
index b0009103..5426c403 100644
--- a/fiesta/apps/files/views.py
+++ b/fiesta/apps/files/views.py
@@ -32,7 +32,7 @@ def has_permission(self, request: HttpRequest, name: str) -> bool:
@classmethod
def as_url(cls, storage: NamespacedFilesStorage, url_name: str = None) -> RoutePattern:
return path(
- f"serve/{storage.namespace}/",
+ f"serve/{storage.namespace}/{url_name or 'default'}/",
cls.as_view(
storage=storage,
),
diff --git a/fiesta/apps/plugins/migrations/0014_alter_plugin_app_label.py b/fiesta/apps/plugins/migrations/0014_alter_plugin_app_label.py
new file mode 100644
index 00000000..1fed9e4f
--- /dev/null
+++ b/fiesta/apps/plugins/migrations/0014_alter_plugin_app_label.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.4 on 2023-09-02 12:04
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('plugins', '0013_alter_plugin_app_label'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='plugin',
+ name='app_label',
+ field=models.CharField(choices=[('buddy_system', 'Buddy System'), ('dashboard', 'Dashboard'), ('sections', 'ESN section'), ('esncards', 'ESNcard'), ('pages', 'Pages')], help_text='Defines system application, which specific plugin represents.', max_length=256, verbose_name='app label'),
+ ),
+ ]
diff --git a/fiesta/apps/sections/migrations/0015_alter_sectionsconfiguration_required_gender_and_more.py b/fiesta/apps/sections/migrations/0015_alter_sectionsconfiguration_required_gender_and_more.py
new file mode 100644
index 00000000..55aff879
--- /dev/null
+++ b/fiesta/apps/sections/migrations/0015_alter_sectionsconfiguration_required_gender_and_more.py
@@ -0,0 +1,38 @@
+# Generated by Django 4.2.4 on 2023-09-02 12:04
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('sections', '0014_alter_sectionmembership_role'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='sectionsconfiguration',
+ name='required_gender',
+ field=models.BooleanField(blank=True, default=None, help_text='Flag if field is needed to fill in user profile: True=field is required, False=field is optional, None=field is not available', null=True, verbose_name='required gender'),
+ ),
+ migrations.AlterField(
+ model_name='sectionsconfiguration',
+ name='required_interests',
+ field=models.BooleanField(blank=True, default=None, help_text='Flag if field is needed to fill in user profile: True=field is required, False=field is optional, None=field is not available', null=True, verbose_name='required interests'),
+ ),
+ migrations.AlterField(
+ model_name='sectionsconfiguration',
+ name='required_nationality',
+ field=models.BooleanField(blank=True, default=None, help_text='Flag if field is needed to fill in user profile: True=field is required, False=field is optional, None=field is not available', null=True, verbose_name='required nationality'),
+ ),
+ migrations.AlterField(
+ model_name='sectionsconfiguration',
+ name='required_phone_number',
+ field=models.BooleanField(blank=True, default=None, help_text='Flag if field is needed to fill in user profile: True=field is required, False=field is optional, None=field is not available', null=True, verbose_name='required phone number'),
+ ),
+ migrations.AlterField(
+ model_name='sectionsconfiguration',
+ name='required_picture',
+ field=models.BooleanField(blank=True, default=None, help_text='Flag if field is needed to fill in user profile: True=field is required, False=field is optional, None=field is not available', null=True, verbose_name='required profile picture'),
+ ),
+ ]
diff --git a/fiesta/apps/sections/templates/sections/parts/section_stats_buddy_btn.html b/fiesta/apps/sections/templates/sections/parts/section_stats_buddy_btn.html
index 20efde9a..adf45f7c 100644
--- a/fiesta/apps/sections/templates/sections/parts/section_stats_buddy_btn.html
+++ b/fiesta/apps/sections/templates/sections/parts/section_stats_buddy_btn.html
@@ -1,3 +1,3 @@
{% load i18n %}
{% trans "show all" %}
+ href="{% url 'buddy_system:requests' %}?matcher_faculty={{ record.pk }}">{% trans "show all" %}
diff --git a/fiesta/apps/sections/views/members.py b/fiesta/apps/sections/views/members.py
index 390fae7c..9629dfa2 100644
--- a/fiesta/apps/sections/views/members.py
+++ b/fiesta/apps/sections/views/members.py
@@ -12,6 +12,7 @@
from django_filters import CharFilter, ChoiceFilter, ModelChoiceFilter
from django_tables2 import Column, TemplateColumn
+from apps.buddy_system.models import BuddyRequest
from apps.buddy_system.views.editor import BuddyRequestsTable
from apps.fiestaforms.views.htmx import HtmxFormMixin
from apps.fiestatables.columns import ImageColumn, NaturalDatetimeColumn
@@ -161,10 +162,10 @@ def get_tables(self):
return [
BuddyRequestsTable(
request=self.request,
- data=self.object.user.buddy_system_matched_requests.all(),
+ data=BuddyRequest.objects.filter(match__matcher=self.object.user),
exclude=(
- "matched_by_name",
- "matched_by_picture",
+ "matcher_name",
+ "matcher_picture",
"match_request",
),
),
diff --git a/fiesta/apps/sections/views/stats.py b/fiesta/apps/sections/views/stats.py
index 41b01088..ea630852 100644
--- a/fiesta/apps/sections/views/stats.py
+++ b/fiesta/apps/sections/views/stats.py
@@ -29,7 +29,7 @@ def qs(self):
qs = super().qs
request_counting_qs = BuddyRequest.objects.filter(
- matched_by__profile__home_faculty=OuterRef("pk"),
+ match__matcher__profile__home_faculty=OuterRef("pk"),
)
# TODO: weird, filtering via self.filters keeps lookup_expr as exact
@@ -43,7 +43,7 @@ def qs(self):
return qs.annotate(
matched_buddy_requests=Coalesce(
Subquery(
- request_counting_qs.values("matched_by__profile__home_faculty")
+ request_counting_qs.values("match__matcher__profile__home_faculty")
.annotate(count=Count("pk"))
.values("count"),
output_field=models.IntegerField(),
diff --git a/fiesta/apps/utils/factories/buddy_system.py b/fiesta/apps/utils/factories/buddy_system.py
index 30e901b2..1e2eb35f 100644
--- a/fiesta/apps/utils/factories/buddy_system.py
+++ b/fiesta/apps/utils/factories/buddy_system.py
@@ -38,7 +38,7 @@ class Meta:
),
)
- description = factory.Faker("text", max_nb_chars=600)
+ note = factory.Faker("text", max_nb_chars=600)
class BuddyRequestWithKnownUserFactory(BuddyRequestWithUserFactory):
diff --git a/fiesta/apps/utils/management/commands/loadlegacydata.py b/fiesta/apps/utils/management/commands/loadlegacydata.py
index 65567333..7086d8b2 100644
--- a/fiesta/apps/utils/management/commands/loadlegacydata.py
+++ b/fiesta/apps/utils/management/commands/loadlegacydata.py
@@ -17,7 +17,7 @@
from apps.accounts.hashers import LegacyBCryptSHA256PasswordHasher
from apps.accounts.models import User, UserProfile
-from apps.buddy_system.models import BuddyRequest
+from apps.buddy_system.models import BuddyRequest, BuddyRequestMatch
from apps.sections.models import Section, SectionMembership, SectionUniversity
from apps.universities.models import Faculty, University
@@ -73,17 +73,24 @@ def load_requests(*, cursor: CursorWrapper):
responsible_section = Section.objects.filter(universities__abbr=issuer_university).first()
- BuddyRequest.objects.update_or_create(
+ br, _ = BuddyRequest.objects.update_or_create(
issuer=ID_TO_USER[issuer_email],
responsible_section=responsible_section,
defaults=dict(
- description=description,
- matched_by=ID_TO_USER.get(matched_by_email),
- matched_at=make_aware(matched_at) if matched_at else None,
+ note=description,
state=BuddyRequest.State.MATCHED if matched_by_email else BuddyRequest.State.CREATED,
),
)
+ if matched_by_email:
+ BuddyRequestMatch.objects.create_or_update(
+ request=br,
+ defaults=dict(
+ matcher=ID_TO_USER[matched_by_email],
+ created=make_aware(matched_at) if matched_at else None,
+ ),
+ )
+
secho("Processing {i: >4}: {desc}.".format(i=i, desc=description[:32].replace("\n", " ")))
diff --git a/fiesta/apps/utils/templatetags/utils.py b/fiesta/apps/utils/templatetags/utils.py
index 02269560..36a9de47 100644
--- a/fiesta/apps/utils/templatetags/utils.py
+++ b/fiesta/apps/utils/templatetags/utils.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import datetime
+import hashlib
import typing
from collections.abc import Reversible
from operator import attrgetter
@@ -58,3 +59,20 @@ def zip_(value, another):
@register.filter
def single_unit_timeuntil(v):
return timeuntil(v, depth=1)
+
+
+@register.filter
+def get_color_by_text(name: typing.Any) -> str:
+ hash_object = hashlib.md5(str(name).encode(), usedforsecurity=False)
+ hash_hex = hash_object.hexdigest()
+
+ r = int(hash_hex[0:2], 16)
+ g = int(hash_hex[2:4], 16)
+ b = int(hash_hex[4:6], 16)
+
+ if r + g + b < 100:
+ r += 30
+ g += 30
+ b += 30
+
+ return f"rgb({r}, {g}, {b})"
diff --git a/fiesta/templates/fiesta/parts/user_status.html b/fiesta/templates/fiesta/parts/user_status.html
index f36437a4..493861ca 100644
--- a/fiesta/templates/fiesta/parts/user_status.html
+++ b/fiesta/templates/fiesta/parts/user_status.html
@@ -1,4 +1,5 @@
{% load user_profile %}
+{% load utils %}
{% if request.user.is_authenticated %}