diff --git a/Makefile b/Makefile index 9c429d30..59d07da5 100644 --- a/Makefile +++ b/Makefile @@ -61,7 +61,7 @@ loadlegacydata: DA_CMD = loadlegacydata ## Loads all data from legacydb run fro loadlegacydata: DCFLAGS = --profile migration loadlegacydata: da -dumpdata: DA_CMD = dumpdata --exclude auth --exclude contenttypes --exclude sessions --exclude sites --exclude admin +dumpdata: DA_CMD = dumpdata --exclude auth --exclude contenttypes --exclude sessions --exclude sites --exclude admin --natural-foreign dumpdata: da fixture ?= diff --git a/fiesta/apps/accounts/migrations/0017_alter_userprofile_instagram.py b/fiesta/apps/accounts/migrations/0017_alter_userprofile_instagram.py new file mode 100644 index 00000000..594c9ef8 --- /dev/null +++ b/fiesta/apps/accounts/migrations/0017_alter_userprofile_instagram.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.4 on 2023-09-02 12:04 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0016_userprofile_facebook_userprofile_instagram_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='userprofile', + name='instagram', + field=models.CharField(blank=True, validators=[django.core.validators.RegexValidator('^[\\w\\-_.]+$')], verbose_name='instagram username'), + ), + ] 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/accounts/templates/accounts/dashboard_block.html b/fiesta/apps/accounts/templates/accounts/dashboard_block.html index 88b49349..dd6da351 100644 --- a/fiesta/apps/accounts/templates/accounts/dashboard_block.html +++ b/fiesta/apps/accounts/templates/accounts/dashboard_block.html @@ -7,11 +7,9 @@ {% compute_profile_fullness user as fullness %} {% interpolate_to_list fullness "text-red-400" "text-orange-400" "text-blue-400" "text-lime-400" as color %}
- {# TODO: compute completness #} - {# TODO: interpolate to color #}
{{ fullness|multiply:100|int }}% 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/apps.py b/fiesta/apps/buddy_system/apps.py index 2bc8e7aa..b307a943 100644 --- a/fiesta/apps/buddy_system/apps.py +++ b/fiesta/apps/buddy_system/apps.py @@ -30,13 +30,26 @@ class BuddySystemConfig(BasePluginAppConfig): 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) + base = ( + super() + .as_navigation_item(request, bound_plugin) + ._replace( + children=( + [ + NavigationItemSpec(title=_("My Buddies"), url=reverse("buddy_system:my-buddies")), + ] + if request.membership.is_local + else [] + ), + ) + ) + if not request.membership.is_privileged: return base return base._replace( - url="", - children=[ + children=base.children + + [ NavigationItemSpec(title=_("Requests"), url=reverse("buddy_system:requests")), ], ) diff --git a/fiesta/apps/buddy_system/forms.py b/fiesta/apps/buddy_system/forms.py index e63be5ae..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 = ( - "description", + "note", "interests", "responsible_section", "issuer", @@ -42,12 +41,12 @@ class Meta: "issuer": HiddenInput, } labels = { - "description": _("Tell us about yourself"), + "note": _("Tell us about yourself"), "interests": _("What are you into?"), } help_texts = { - "description": lazy( - lambda: render_to_string("buddy_system/parts/buddy_request_description_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["description"].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", - "description", + "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/0020_rename_description_buddyrequest_issuer_note.py b/fiesta/apps/buddy_system/migrations/0020_rename_description_buddyrequest_issuer_note.py new file mode 100644 index 00000000..3088706c --- /dev/null +++ b/fiesta/apps/buddy_system/migrations/0020_rename_description_buddyrequest_issuer_note.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.4 on 2023-09-02 12:04 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('buddy_system', '0019_alter_buddyrequest_options'), + ] + + operations = [ + migrations.RenameField( + model_name='buddyrequest', + old_name='description', + new_name='issuer_note', + ), + ] 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 546857d0..408ac6ae 100644 --- a/fiesta/apps/buddy_system/templates/buddy_system/dashboard_block.html +++ b/fiesta/apps/buddy_system/templates/buddy_system/dashboard_block.html @@ -1,4 +1,5 @@ {% load buddy_system %} +{% load utils %} {% load i18n %} {% load user_profile %}
@@ -19,27 +20,41 @@ There is {{ waiting_total }} waiting request{{ waiting_total|pluralize:"s" }} before yours.
{% elif br.state == br.State.MATCHED %} - {% get_user_picture br.matched_by as buddy_picture %} + {% get_user_picture br.match.matcher as buddy_picture %} +
+ {% trans "find out about your buddy" %} - {% if buddy_picture %} -
-
-
- +
+ {% if buddy_picture %} + Matched buddy picture -
+ {% else %} + {{ br.match.matcher.first_name|first }}{{ br.match.matcher.last_name|first }} + {% endif %}
- {% endif %} + +
+
✅ Matched
-
It's a match!
+
+ 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 %} + {% with waiting_brs.count as count %} + {{ count }} + waiting request{{ count|pluralize:"s" }} + {% endwith %}
My Buddies
- {% get_matched_buddy_requests as matched_requests %} - {% if not matched_requests.exists %} + {% get_matched_buddy_requests as request_matches %} + {% if not request_matches.exists %}
So empty here :(
- {% 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 %} - + {% else %} - + {% 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 %} -
-

My requests

- {# TODO: ugly #} - - {% for request in requests %} - - - - - - - {% empty %} - - - - {% endfor %} -
{{ request }}{{ request.responsible_section }}{{ request.created }}{{ request.state }}
- new buddy request -
-
+ + {% 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 %} + + + new buddy request + + + {% 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">
+ hx-boost="true" + class="flex flex-col space-y-4" + x-data="{ note: '', noteEnabled: $el.dataset.enableNoteFromMatcher === 'true' }" + data-enable-note-from-matcher="{{ configuration.enable_note_from_matcher|lower }}"> {% csrf_token %}
-

{% blocktrans %}Buddy Request Confirmation{% endblocktrans %}

+

{% blocktrans %}Buddy Request Confirmation{% endblocktrans %}

-

+

{% blocktrans %}Are you sure you want to confirm the buddy request, acknowledging that you will be responsible for being buddy?{% 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 %} + -
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 }} + +

+ +
+ +
+ {{ 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.issuer.profile.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.email %} + + ✉️ + E-mail + + {% endif %} +
+ {% 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 %}
{% else %} - - - - + {{ request.user.first_name|first }}{{ request.user.last_name|first }} {% endif %}