From cba11550ad476582ea4de8317815ef49e9c71d33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Sun, 14 May 2023 17:21:00 -0300 Subject: [PATCH 01/27] Create model for user permissions This commit creates a new model for assigning specific permissions per tournament to a user, with an addition to Edit Database for assigning them. To start, some permissions relating to editing or accessing conflicts was added. The maximum length was put at 50 to allow for some expansion. The current permissions follow a pattern of "edit"/"view".model for consistency with the action and where. In addition, a new helper function is created to determine whether a given user has the requested permission. These permissions will only apply to logged-in users as unauthenticated access to various parts of the platform is already handled through settings. Further, superusers will bypass permissions. This is to maintain backwards compatibility, but also as a step towards deprecating the Assistant view (used by authenticated users without advanced privileges) towards them having a restricted access to the Admin interface. As permissions may be frequently accessed, they'll be cached similarly to tournaments in TournamentsMixin, using an attribute on the user, then through caching, until fetching from the DB. There's also a wrapper for the method, to be used within templates. --- tabbycat/users/admin.py | 7 ++++ tabbycat/users/migrations/0001_initial.py | 32 +++++++++++++++++ tabbycat/users/migrations/__init__.py | 0 tabbycat/users/models.py | 19 ++++++++++ tabbycat/users/permissions.py | 42 ++++++++++++++++++++++ tabbycat/utils/templatetags/debate_tags.py | 7 ++++ 6 files changed, 107 insertions(+) create mode 100644 tabbycat/users/migrations/0001_initial.py create mode 100644 tabbycat/users/migrations/__init__.py create mode 100644 tabbycat/users/models.py create mode 100644 tabbycat/users/permissions.py diff --git a/tabbycat/users/admin.py b/tabbycat/users/admin.py index a40d384d033..73813dbcf3a 100644 --- a/tabbycat/users/admin.py +++ b/tabbycat/users/admin.py @@ -5,6 +5,8 @@ from django.contrib.auth.models import Group from django.utils.translation import gettext_lazy as _ +from .models import UserPermission + # ============================================================================== # Authentication and Authorization @@ -55,6 +57,11 @@ class UserAdmin(BaseUserAdmin): form = UserChangeFormExtended +@admin.register(UserPermission) +class UserPermissionAdmin(admin.ModelAdmin): + list_display = ('user', 'permission', 'tournament') + + User = get_user_model() admin.site.unregister(User) admin.site.register(User, UserAdmin) diff --git a/tabbycat/users/migrations/0001_initial.py b/tabbycat/users/migrations/0001_initial.py new file mode 100644 index 00000000000..5ef1152a6ee --- /dev/null +++ b/tabbycat/users/migrations/0001_initial.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.1 on 2023-05-14 16:59 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('tournaments', '0010_alter_round_draw_type'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='UserPermission', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('permission', models.CharField(choices=[('view.adjudicatorteamconflict', 'view adjudicator-team conflicts'), ('edit.adjudicatorteamconflict', 'edit adjudicator-team conflicts'), ('view.adjudicatoradjudicatorconflict', 'view adjudicator-adjudicator conflicts'), ('edit.adjudicatoradjudicatorconflict', 'edit adjudicator-adjudicator conflicts'), ('view.adjudicatorinstitutionconflict', 'view adjudicator-institution conflicts'), ('edit.adjudicatorinstitutionconflict', 'edit adjudicator-institution conflicts'), ('view.teaminstitutionconflict', 'view team-institution conflicts'), ('edit.teaminstitutionconflict', 'edit team-institution conflicts')], verbose_name='permission')), + ('tournament', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tournaments.tournament', verbose_name='tournament')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')), + ], + options={ + 'verbose_name': 'user permission', + 'verbose_name_plural': 'user permissions', + 'unique_together': {('user', 'permission', 'tournament')}, + }, + ), + ] diff --git a/tabbycat/users/migrations/__init__.py b/tabbycat/users/migrations/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tabbycat/users/models.py b/tabbycat/users/models.py new file mode 100644 index 00000000000..c49691cb612 --- /dev/null +++ b/tabbycat/users/models.py @@ -0,0 +1,19 @@ +from django.conf import settings +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from .permissions import Permission + + +class UserPermission(models.Model): + user = models.ForeignKey(settings.AUTH_USER_MODEL, models.CASCADE, verbose_name=_("user")) + permission = models.CharField(max_length=50, choices=Permission.choices, verbose_name=_("permission")) + tournament = models.ForeignKey('tournaments.Tournament', models.CASCADE, verbose_name=_("tournament")) + + class Meta: + verbose_name = _("user permission") + verbose_name_plural = _("user permissions") + unique_together = [('user', 'permission', 'tournament')] + + def __str__(self): + return "%s: %s (%s)" % (self.user.username, self.permission, self.tournament.slug) diff --git a/tabbycat/users/permissions.py b/tabbycat/users/permissions.py new file mode 100644 index 00000000000..508dab3489a --- /dev/null +++ b/tabbycat/users/permissions.py @@ -0,0 +1,42 @@ +from django.core.cache import cache +from django.db.models import TextChoices +from django.utils.translation import gettext_lazy as _ + + +def has_permission(user, permission, tournament): + if user.is_superuser: + return True + + if isinstance(permission, bool): + return permission + + if not hasattr(user, '_permissions'): + user._permissions = {} + + if tournament.slug in user._permissions: + return permission in user._permissions[tournament.slug] + else: + user._permissions[tournament.slug] = set() + + cached_perm = cache.get("user_%d_%s_%s_permission" % (user.pk, tournament.slug, str(permission))) + if cached_perm is not None: + if cached_perm: + user._permissions[tournament.slug].add(permission) + return cached_perm + + perm = user.userpermission_set.filter(permission=permission, tournament=tournament).exists() + cache.set("user_%d_%s_%s_permission" % (user.pk, tournament.slug, str(permission)), perm) + if perm: + user._permissions[tournament.slug].add(permission) + return perm + + +class Permission(TextChoices): + VIEW_ADJ_TEAM_CONFLICTS = 'view.adjudicatorteamconflict', _("view adjudicator-team conflicts") + EDIT_ADJ_TEAM_CONFLICTS = 'edit.adjudicatorteamconflict', _("edit adjudicator-team conflicts") + VIEW_ADJ_ADJ_CONFLICTS = 'view.adjudicatoradjudicatorconflict', _("view adjudicator-adjudicator conflicts") + EDIT_ADJ_ADJ_CONFLICTS = 'edit.adjudicatoradjudicatorconflict', _("edit adjudicator-adjudicator conflicts") + VIEW_ADJ_INST_CONFLICTS = 'view.adjudicatorinstitutionconflict', _("view adjudicator-institution conflicts") + EDIT_ADJ_INST_CONFLICTS = 'edit.adjudicatorinstitutionconflict', _("edit adjudicator-institution conflicts") + VIEW_TEAM_INST_CONFLICTS = 'view.teaminstitutionconflict', _("view team-institution conflicts") + EDIT_TEAM_INST_CONFLICTS = 'edit.teaminstitutionconflict', _("edit team-institution conflicts") diff --git a/tabbycat/utils/templatetags/debate_tags.py b/tabbycat/utils/templatetags/debate_tags.py index a774288a6eb..d6bba80b22a 100644 --- a/tabbycat/utils/templatetags/debate_tags.py +++ b/tabbycat/utils/templatetags/debate_tags.py @@ -8,6 +8,7 @@ from django.template.defaulttags import URLNode from tournaments.utils import get_side_name +from users.permissions import has_permission register = template.Library() STATIC_PATH = settings.MEDIA_ROOT @@ -241,3 +242,9 @@ def abbreviatename(name): """Takes a two-part name and returns an abbreviation like 'E.Luฤiฤ‡'.""" parts = name.split(" ") return "%s.%s" % (parts[0][:5], parts[-1][:5]) # Used for barcodes + + +@register.simple_tag(takes_context=True) +def haspermission(context, permission): + # If returned directly from the object it will have to lookup tournament + return has_permission(context['user'], permission, context['tournament']) From 341ff0185ddc8cea9c1f352b7b060504c88f9b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Mon, 15 May 2023 18:36:34 -0300 Subject: [PATCH 02/27] Implement permissions for checking conflicts This commit implements the conflicts permissions to view and edit conflicts from the admin area, disabling all fields and removing the "Delete" checkboxes if in read-only. As a consequence of adding the permissions check to the admin mixin, all views will be disabled for non-superusers, until a permission has been added to the view, or the permission was set to `True`. --- .../templates/edit_conflicts.html | 6 +++- tabbycat/adjallocation/views.py | 28 ++++++++++++++++--- tabbycat/templates/components/formset.html | 4 +-- tabbycat/utils/mixins.py | 14 ++++++++++ 4 files changed, 45 insertions(+), 7 deletions(-) diff --git a/tabbycat/adjallocation/templates/edit_conflicts.html b/tabbycat/adjallocation/templates/edit_conflicts.html index 9d3c8211d06..2d6e13ec22d 100644 --- a/tabbycat/adjallocation/templates/edit_conflicts.html +++ b/tabbycat/adjallocation/templates/edit_conflicts.html @@ -1,5 +1,9 @@ {% extends "base.html" %} {% block content %} - {% include "components/formset.html" with triple=True %} + {% if can_edit %} + {% include "components/formset.html" with triple=True %} + {% else %} + {% include "components/formset.html" with double=True %} + {% endif %} {% endblock content %} diff --git a/tabbycat/adjallocation/views.py b/tabbycat/adjallocation/views.py index 83f37acae0d..fed70adbd48 100644 --- a/tabbycat/adjallocation/views.py +++ b/tabbycat/adjallocation/views.py @@ -13,6 +13,7 @@ from participants.models import Adjudicator, Region from participants.prefetch import populate_feedback_scores from tournaments.mixins import DebateDragAndDropMixin, TournamentMixin +from users.permissions import has_permission from utils.misc import ranks_dictionary, redirect_tournament, reverse_tournament from utils.mixins import AdministratorMixin from utils.views import ModelFormSetView @@ -143,13 +144,32 @@ class BaseAdjudicatorConflictsView(LogActionMixin, AdministratorMixin, Tournamen template_name = 'edit_conflicts.html' page_emoji = "๐Ÿ”ถ" - formset_factory_kwargs = { - 'extra': 10, - 'can_delete': True, - } + formset_factory_kwargs = {} + + def get_view_permission(self): + return 'view.%s' % (self.formset_model.__name__.lower()) + + def get_edit_permission(self): + return 'edit.%s' % (self.formset_model.__name__.lower()) + + def get_formset_factory_kwargs(self): + can_edit = has_permission(self.request.user, self.get_edit_permission(), self.tournament) + kwargs = super().get_formset_factory_kwargs() + kwargs['extra'] = 10 * int(can_edit) + kwargs['can_delete'] = can_edit + return kwargs + + def get_formset(self): + formset = super().get_formset() + if not has_permission(self.request.user, self.get_edit_permission(), self.tournament): + for form in formset: + for field in form.fields.values(): + field.disabled = True + return formset def get_context_data(self, **kwargs): kwargs['save_text'] = self.save_text + kwargs['can_edit'] = has_permission(self.request.user, self.get_edit_permission(), self.tournament) return super().get_context_data(**kwargs) def get_success_url(self, *args, **kwargs): diff --git a/tabbycat/templates/components/formset.html b/tabbycat/templates/components/formset.html index d5169de47ad..0c19b37543e 100644 --- a/tabbycat/templates/components/formset.html +++ b/tabbycat/templates/components/formset.html @@ -17,10 +17,10 @@
- +
- +
diff --git a/tabbycat/utils/mixins.py b/tabbycat/utils/mixins.py index cd327220d39..4268fd3e733 100644 --- a/tabbycat/utils/mixins.py +++ b/tabbycat/utils/mixins.py @@ -8,6 +8,8 @@ from django.views.decorators.cache import cache_page from django.views.generic.base import ContextMixin +from users.permissions import has_permission + logger = logging.getLogger(__name__) @@ -51,12 +53,24 @@ class AdministratorMixin(UserPassesTestMixin, ContextMixin): Requires user to be a superuser.""" view_role = "admin" for_admin = True + view_permission = None + edit_permission = None def get_context_data(self, **kwargs): kwargs["user_role"] = self.view_role return super().get_context_data(**kwargs) + def get_view_permission(self): + return self.view_permission + + def get_edit_permission(self): + return self.edit_permission + def test_func(self): + if self.request.method == 'GET' and self.get_view_permission() is not None: + return has_permission(self.request.user, self.get_view_permission(), self.tournament) + if self.request.method == 'POST' and self.get_edit_permission() is not None: + return has_permission(self.request.user, self.get_edit_permission(), self.tournament) return self.request.user.is_superuser From d24f1c3d979842169527d13673a5f66b07b2c0c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Tue, 16 May 2023 23:13:47 -0300 Subject: [PATCH 03/27] Create path to access conflicts for auth'd users This commit creates a path for authenticated users to click through links on Tabbycat until they reach a page which requires authorization (currently the conflicts pages), without encountering a 401/403. This means that the admin overview and Simple Importer index pages need to be accessible for all logged-in users, although all data seen in the overview page would be hidden behind permissions. For this, many new permissions have been added from those which are used in the overview, plus some extra ones that sprang to mind. These haven't been added to the migration yet, but choices are not set at the DB level anyways. --- tabbycat/importer/views.py | 1 + .../templates/TournamentOverviewContainer.vue | 33 +++++++++++----- .../templates/assistant_tournament_index.html | 4 +- .../templates/tournament_index.html | 15 ++++--- .../templates/tournament_index_links.html | 4 +- tabbycat/tournaments/views.py | 39 +++++++++++++------ tabbycat/users/permissions.py | 35 +++++++++++++++++ 7 files changed, 102 insertions(+), 29 deletions(-) diff --git a/tabbycat/importer/views.py b/tabbycat/importer/views.py index d1f77379df8..12aeacf59c1 100644 --- a/tabbycat/importer/views.py +++ b/tabbycat/importer/views.py @@ -40,6 +40,7 @@ class ImporterSimpleIndexView(AdministratorMixin, TournamentMixin, TemplateView): template_name = 'simple_import_index.html' + view_permission = True class BaseImportWizardView(AdministratorMixin, LogActionMixin, TournamentMixin, SessionWizardView): diff --git a/tabbycat/tournaments/templates/TournamentOverviewContainer.vue b/tabbycat/tournaments/templates/TournamentOverviewContainer.vue index 449b775cece..307bdc11313 100644 --- a/tabbycat/tournaments/templates/TournamentOverviewContainer.vue +++ b/tabbycat/tournaments/templates/TournamentOverviewContainer.vue @@ -9,11 +9,12 @@
    -
  • +
  • +
@@ -27,12 +28,13 @@
-
    +
      -
    • +
    +
      +
    @@ -42,12 +44,13 @@
    -
      +
        -
      • +
      +
        +
      @@ -68,19 +71,31 @@ export default { UpdatesList, BallotsGraph: () => import('../../templates/graphs/BallotsGraph.vue'), }, - props: ['tournamentSlug', 'totalDebates', 'initialActions', 'initialBallots', 'initialGraphData'], + props: ['tournamentSlug', 'totalDebates', 'initialActions', 'initialBallots', 'initialGraphData', 'permissions'], data: function () { return { actionLogs: this.initialActions, ballotResults: this.initialBallots, ballotStatuses: this.initialGraphData, - sockets: ['action_logs', 'ballot_results', 'ballot_statuses'], } }, computed: { tournamentSlugForWSPath: function () { return this.tournamentSlug }, + sockets: function () { + const sockets = [] + if (this.permissions.actionlog) { + sockets.push('action_logs') + } + if (this.permissions.graph) { + sockets.push('ballot_statuses') + } + if (this.permissions.results) { + sockets.push('ballot_results') + } + return sockets + }, }, methods: { handleSocketReceive: function (socketLabel, payload) { diff --git a/tabbycat/tournaments/templates/assistant_tournament_index.html b/tabbycat/tournaments/templates/assistant_tournament_index.html index 4728aba9e43..6f9475e97b9 100644 --- a/tabbycat/tournaments/templates/assistant_tournament_index.html +++ b/tabbycat/tournaments/templates/assistant_tournament_index.html @@ -22,7 +22,8 @@ :initial-actions="initialActions" :initial-ballots="initialBallots" :initial-graph-data="initialGraphData" - :total-debates="totalDebates"> + :total-debates="totalDebates" + :permissions="permissions"> @@ -36,6 +37,7 @@ initialActions: {{ initialActions|safe }}, initialBallots: {{ initialBallots|safe }}, initialGraphData: {{ initial_graph_data|safe }}, + permissions: {{ overview_permissions|safe }} } {{ block.super }} diff --git a/tabbycat/tournaments/templates/tournament_index.html b/tabbycat/tournaments/templates/tournament_index.html index 27486722f31..f9d08a1525e 100644 --- a/tabbycat/tournaments/templates/tournament_index.html +++ b/tabbycat/tournaments/templates/tournament_index.html @@ -17,7 +17,11 @@
      {% endif %} - {% if blank %} + {% haspermission "add.team" as teams_perm %} + {% haspermission "edit.roundavailability.team" as availability_perm %} + {% haspermission "edit.debateadjudicator" as allocation_perm %} + {% haspermission "add.ballotsubmission" as results_perm %} + {% if blank and teams_perm %}
      @@ -45,7 +49,7 @@

      {% trans "Welcome to your new tournament!" %}

      {% include "components/item-action.html" with to_complete=True %}
      - {% elif round.draw_status == round.Status.NONE %} + {% elif round.draw_status == round.Status.NONE and availability_perm %}
      {% roundurl 'availability-index' round as url %} @@ -55,7 +59,7 @@

      {% trans "Welcome to your new tournament!" %}

      {% include "components/item-action.html" with to_complete=True %}
      - {% elif round.draw_status == round.Status.CONFIRMED %} + {% elif round.draw_status == round.Status.CONFIRMED and allocation_perm %}
      {% roundurl 'draw' round as url %} @@ -65,7 +69,7 @@

      {% trans "Welcome to your new tournament!" %}

      {% include "components/item-action.html" with to_complete=True %}
      - {% elif round.draw_status == round.Status.RELEASED %} + {% elif round.draw_status == round.Status.RELEASED and results_perm %}
      {% roundurl 'results-round-list' round as url %} @@ -77,7 +81,8 @@

      {% trans "Welcome to your new tournament!" %}

      {% endif %} - {% if global_preferences.accounts__admin_account_key or global_preferences.accounts__assistant_account_key %} + {% haspermission "edit.tournamentoptionmodel" as options_perm %} + {% if global_preferences.accounts__admin_account_key or global_preferences.accounts__assistant_account_key and options_perm %} {% tournamenturl 'options-tournament-section' section='global' as url %} {% blocktrans trimmed asvar text %} You have enabled a URL that allows users to create new accounts. You should disable it once the tournament starts. diff --git a/tabbycat/tournaments/templates/tournament_index_links.html b/tabbycat/tournaments/templates/tournament_index_links.html index 8f39d0eb03d..8f54d7d90b9 100644 --- a/tabbycat/tournaments/templates/tournament_index_links.html +++ b/tabbycat/tournaments/templates/tournament_index_links.html @@ -6,14 +6,12 @@ {% url 'tournament-assistant-home' tournament_slug=tournament.slug as assistant_url %} {% url 'tournament-public-index' tournament_slug=tournament.slug as public_url %} - {% if user.is_superuser %} + {% if user.is_authenticated %} {% blocktrans trimmed asvar text with tn=tournament.name %} Administrator area for {{ tn }} {% endblocktrans %} {% include "components/item-action.html" with icon="settings" type=type subtext=subtext url=admin_url %} - {% endif %} - {% if user.is_authenticated %} {% blocktrans trimmed asvar text with tn=tournament.name %} Assistant area for {{ tn }} {% endblocktrans %} diff --git a/tabbycat/tournaments/views.py b/tabbycat/tournaments/views.py index 28b7984f0d7..68f1341295e 100644 --- a/tabbycat/tournaments/views.py +++ b/tabbycat/tournaments/views.py @@ -19,6 +19,7 @@ from results.models import BallotSubmission from results.prefetch import populate_confirmed_ballots from tournaments.models import Round +from users.permissions import has_permission from utils.misc import redirect_round, redirect_tournament, reverse_round, reverse_tournament from utils.mixins import (AdministratorMixin, AssistantMixin, CacheMixin, TabbycatPageTitlesMixin, WarnAboutDatabaseUseMixin, WarnAboutLegacySendgridConfigVarsMixin) @@ -76,20 +77,29 @@ def get_context_data(self, **kwargs): kwargs["readthedocs_version"] = settings.READTHEDOCS_VERSION kwargs["blank"] = not (t.team_set.exists() or t.adjudicator_set.exists() or t.venue_set.exists()) - actions = ActionLogEntry.objects.filter(tournament=t).prefetch_related( - 'content_object', 'user').order_by('-timestamp')[:updates] - kwargs["initialActions"] = json.dumps([a.serialize for a in actions]) - - debates = t.current_round.debate_set.filter( - ballotsubmission__confirmed=True, - ).order_by('-ballotsubmission__timestamp')[:updates] - populate_confirmed_ballots(debates, results=True) - subs = [d._confirmed_ballot.serialize_like_actionlog for d in debates] - kwargs["initialBallots"] = json.dumps(subs) + action_perm = has_permission(self.request.user, 'view.actionlogentry', self.tournament) + if action_perm: + actions = ActionLogEntry.objects.filter(tournament=t).prefetch_related( + 'content_object', 'user').order_by('-timestamp')[:updates] + kwargs["initialActions"] = json.dumps([a.serialize for a in actions]) + else: + kwargs["initialActions"] = json.dumps([]) + + results_perm = has_permission(self.request.user, 'view.ballotsubmission', self.tournament) + if results_perm: + debates = t.current_round.debate_set.filter( + ballotsubmission__confirmed=True, + ).order_by('-ballotsubmission__timestamp')[:updates] + populate_confirmed_ballots(debates, results=True) + subs = [d._confirmed_ballot.serialize_like_actionlog for d in debates] + kwargs["initialBallots"] = json.dumps(subs) + else: + kwargs["initialBallots"] = json.dumps([]) status = t.current_round.draw_status kwargs["total_debates"] = t.current_round.debate_set.count() - if status == Round.Status.CONFIRMED or status == Round.Status.RELEASED: + graph_perm = has_permission(self.request.user, 'view.ballotsubmission.graph', self.tournament) + if (status == Round.Status.CONFIRMED or status == Round.Status.RELEASED) and graph_perm: ballots = BallotSubmission.objects.filter( debate__round=t.current_round, discarded=False).select_related( 'submitter', 'debate') @@ -98,6 +108,12 @@ def get_context_data(self, **kwargs): else: kwargs["initial_graph_data"] = json.dumps([]) + kwargs["overview_permissions"] = json.dumps({ + "graph": graph_perm, + "actionlog": action_perm, + "results": results_perm, + }) + return super().get_context_data(**kwargs) @@ -107,6 +123,7 @@ class TournamentAssistantHomeView(AssistantMixin, BaseTournamentDashboardHomeVie class TournamentAdminHomeView(AdministratorMixin, BaseTournamentDashboardHomeView): template_name = 'tournament_index.html' + view_permission = True class CompleteRoundCheckView(AdministratorMixin, RoundMixin, TemplateView): diff --git a/tabbycat/users/permissions.py b/tabbycat/users/permissions.py index 508dab3489a..afc7c08b446 100644 --- a/tabbycat/users/permissions.py +++ b/tabbycat/users/permissions.py @@ -40,3 +40,38 @@ class Permission(TextChoices): EDIT_ADJ_INST_CONFLICTS = 'edit.adjudicatorinstitutionconflict', _("edit adjudicator-institution conflicts") VIEW_TEAM_INST_CONFLICTS = 'view.teaminstitutionconflict', _("view team-institution conflicts") EDIT_TEAM_INST_CONFLICTS = 'edit.teaminstitutionconflict', _("edit team-institution conflicts") + + VIEW_ACTIONLOGENTRIES = 'view.actionlogentry', _("view action log entries") + # EDIT_ACTIONLOGENTRIES omitted as pre-supposed when taking an action + + VIEW_TEAMS = 'view.team', _("view teams") + ADD_TEAMS = 'add.team', _("add teams") + + VIEW_ROUNDAVAILABILITIES_TEAM = 'view.roundavailability.team', _("view round availabilities for teams") + VIEW_ROUNDAVAILABILITIES_ADJ = 'view.roundavailability.adjudicator', _("view round availabilities for adjudicators") + VIEW_ROUNDAVAILABILITIES_VENUE = 'view.roundavailability.venue', _("view round availabilities for rooms") + EDIT_ROUNDAVAILABILITIES_TEAM = 'edit.roundavailability.team', _("edit round availabilities for teams") + EDIT_ROUNDAVAILABILITIES_ADJ = 'edit.roundavailability.adjudicator', _("edit round availabilities for adjudicators") + EDIT_ROUNDAVAILABILITIES_VENUE = 'edit.roundavailability.venue', _("edit round availabilities for rooms") + + VIEW_DEBATES = 'view.debate', _("view debates (draw)") + VIEW_ADMIN_DRAW = 'view.debate.admin', _("view debates (detailed draw)") + GENERATE_DEBATE = 'generate.debate', _("generate debates (draw)") + EDIT_DEBATETEAMS = 'edit.debateteam', _("edit debate teams (pairings)") + VIEW_DEBATEADJUDICATORS = 'view.debateadjudicator', _("view debate adjudicators (allocations)") + EDIT_DEBATEADJUDICATORS = 'edit.debateadjudicator', _("edit debate adjudicators (allocations)") + + # Logic behind the ballotsub permissions: + # Confirmed ballots are more prominent than old ones, but are more sensitive to changes. + # Then, assistants may confirm others' ballots but not their own. + VIEW_NEW_BALLOTSUBMISSIONS = 'view.ballotsubmission.new', _("view confirmed ballots") + EDIT_OLD_BALLOTSUBMISSIONS = 'edit.ballotsubmission.old', _("edit non-confirmed ballots") + VIEW_BALLOTSUBMISSIONS = 'view.ballotsubmission', _("view any ballot") + EDIT_BALLOTSUBMISSIONS = 'edit.ballotsubmission', _("edit any ballot") + ADD_BALLOTSUBMISSIONS = 'add.ballotsubmission', _("create ballots") + MARK_BALLOTSUBMISSIONS = 'mark.ballotsubmission', _("confirm/discard any ballot") + MARK_OTHERS_BALLOTSUBMISSIONS = 'mark.ballotsubmission.others', _("confirm/discard others' ballots") + VIEW_BALLOTSUBMISSION_GRAPH = 'view.ballotsubmission.graph', _("view ballot graph") + + VIEW_TOURNAMENTPREFERENCEMODEL = 'view.tournamentpreferencemodel', _("view tournament configuration") + EDIT_TOURNAMENTPREFERENCEMODEL = 'edit.tournamentpreferencemodel', _("edit tournament configuration") From 8feabf000e9bc431075b42a90900c463c0efae1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Sat, 22 Jul 2023 17:23:32 -0500 Subject: [PATCH 04/27] Create mechanism for grouping permissions This commit creates the models and admin integration to be able to create groupings of permissions and assign them to users. This would facilitate giving standard permissions to different users by the role(s) they have per tournament. Groups are lists of permissions, applied to a specific tournament. This allows groups to be modified without affecting permissions elsewhere. Users are then assigned these groups as Memberships. Individual permissions can also be added through UserPermission objects. There will be default lists of permissions that will form groups on the creation of tournaments: * Equity: Manage conflicts/constraints * Adjudicator Core: Create allocations and motions In addition to a Tab Director role with full access, and "Tab Assistant" with equivalent permissions to what is available on the assistant interface. The DB page for users has also been updated to be localisable, and be able to assign membership and permissions. --- tabbycat/users/admin.py | 40 +++++++++++++---------- tabbycat/users/groups.py | 38 +++++++++++++++++++++ tabbycat/users/migrations/0001_initial.py | 34 +++++++++++++++++-- tabbycat/users/models.py | 27 +++++++++++++++ 4 files changed, 119 insertions(+), 20 deletions(-) create mode 100644 tabbycat/users/groups.py diff --git a/tabbycat/users/admin.py b/tabbycat/users/admin.py index 73813dbcf3a..0e47f97f9f8 100644 --- a/tabbycat/users/admin.py +++ b/tabbycat/users/admin.py @@ -2,57 +2,58 @@ from django.contrib.auth import get_user_model from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.forms import UserChangeForm, UserCreationForm -from django.contrib.auth.models import Group +from django.contrib.auth.models import Group as AuthGroup +from django.forms import ModelMultipleChoiceField from django.utils.translation import gettext_lazy as _ -from .models import UserPermission +from .models import Group, Membership, UserPermission # ============================================================================== # Authentication and Authorization # ============================================================================== -admin.site.unregister(Group) # No need to show groups +admin.site.unregister(AuthGroup) # No need to show groups + + +class UserPermissionInline(admin.TabularInline): + model = UserPermission + fields = ('permission', 'tournament') class CustomUserLabelsMixin: def __init__(self, *args, **kwargs): - super(CustomUserLabelsMixin, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.fields['is_staff'].help_text = _("Users with staff status can" " view and edit the Edit Database area. This is potentially " "dangerous and should be reserved for the actual tab director(s).") - self.fields['is_superuser'].help_text = _("Superusers have full " - "access all areas of Tabbycat necessary to run a tournament. Users " - "who are not superusers are still able to perform data entry tasks " - "such as adding results and feedback but can't access confidential " - "areas such as the Breaks and Feedback sections. Chief adjudicators " - "and their deputies are generally given superuser status if " - "they know what they are doing.") class UserChangeFormExtended(CustomUserLabelsMixin, UserChangeForm): - pass + membership = ModelMultipleChoiceField(queryset=Membership.objects.all(), required=False) class UserCreationFormExtended(CustomUserLabelsMixin, UserCreationForm): - pass + membership = ModelMultipleChoiceField(queryset=Membership.objects.all(), required=False) class UserAdmin(BaseUserAdmin): list_display = ('username', 'email', 'is_active', 'is_staff', 'is_superuser') + inlines = (UserPermissionInline,) fieldsets = ( # Hide groups and user permission fields - ('Personal info', {'fields': ('username', 'email', 'password')}), - ('Important dates', {'fields': ('last_login', 'date_joined')}), - ('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser')}), + (_('Personal info'), {'fields': ('username', 'email', 'password')}), + (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', 'membership')}), + (_('Important dates'), {'fields': ('last_login', 'date_joined')}), ) add_fieldsets = ( # Set permissions when creating (None, { - 'fields': ('username', 'password1', 'password2', 'email', 'is_staff', 'is_superuser'), + 'fields': ('username', 'password1', 'password2', 'email', 'is_staff', 'is_superuser', 'membership'), }), ) + add_form_template = 'admin/change_form.html' add_form = UserCreationFormExtended form = UserChangeFormExtended @@ -62,6 +63,11 @@ class UserPermissionAdmin(admin.ModelAdmin): list_display = ('user', 'permission', 'tournament') +@admin.register(Group) +class GroupAdmin(admin.ModelAdmin): + list_display = ('name', 'tournament') + + User = get_user_model() admin.site.unregister(User) admin.site.register(User, UserAdmin) diff --git a/tabbycat/users/groups.py b/tabbycat/users/groups.py new file mode 100644 index 00000000000..7619ace909a --- /dev/null +++ b/tabbycat/users/groups.py @@ -0,0 +1,38 @@ +from django.utils.translation import gettext_lazy as _ + +from options.presets import _all_subclasses + +from .permissions import Permission + + +def all_groups(): + yield from _all_subclasses(BaseGroup) + + +class BaseGroup: + name = None + permissions = [] + + +class Equity(BaseGroup): + # Permissions to manage conflicts/constraints, view feedback + participant info + name = _("Equity") + permissions = [] + + +class AdjudicationCore(BaseGroup): + # Permissions to make [preformed] allocations, view feedback, and create motions + name = _("Adjudication Core") + permissions = [] + + +class TabDirector(BaseGroup): + # All permissions + name = _("Tabulation Director") + permissions = [p for p in Permission] + + +class TabAssistant(BaseGroup): + # Permissions to match the Assistant interface + name = _("Tabulation Assistant") + permissions = [] diff --git a/tabbycat/users/migrations/0001_initial.py b/tabbycat/users/migrations/0001_initial.py index 5ef1152a6ee..e9c406c5073 100644 --- a/tabbycat/users/migrations/0001_initial.py +++ b/tabbycat/users/migrations/0001_initial.py @@ -1,8 +1,9 @@ -# Generated by Django 4.2.1 on 2023-05-14 16:59 +# Generated by Django 4.1.7 on 2023-07-24 16:24 from django.conf import settings from django.db import migrations, models import django.db.models.deletion +import utils.fields class Migration(migrations.Migration): @@ -10,16 +11,30 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('tournaments', '0010_alter_round_draw_type'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('tournaments', '0010_alter_round_draw_type'), ] operations = [ + migrations.CreateModel( + name='Group', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, verbose_name='name')), + ('permissions', utils.fields.ChoiceArrayField(base_field=models.CharField(choices=[('view.adjudicatorteamconflict', 'view adjudicator-team conflicts'), ('edit.adjudicatorteamconflict', 'edit adjudicator-team conflicts'), ('view.adjudicatoradjudicatorconflict', 'view adjudicator-adjudicator conflicts'), ('edit.adjudicatoradjudicatorconflict', 'edit adjudicator-adjudicator conflicts'), ('view.adjudicatorinstitutionconflict', 'view adjudicator-institution conflicts'), ('edit.adjudicatorinstitutionconflict', 'edit adjudicator-institution conflicts'), ('view.teaminstitutionconflict', 'view team-institution conflicts'), ('edit.teaminstitutionconflict', 'edit team-institution conflicts'), ('view.actionlogentry', 'view action log entries'), ('view.team', 'view teams'), ('add.team', 'add teams'), ('view.roundavailability.team', 'view round availabilities for teams'), ('view.roundavailability.adjudicator', 'view round availabilities for adjudicators'), ('view.roundavailability.venue', 'view round availabilities for rooms'), ('edit.roundavailability.team', 'edit round availabilities for teams'), ('edit.roundavailability.adjudicator', 'edit round availabilities for adjudicators'), ('edit.roundavailability.venue', 'edit round availabilities for rooms'), ('view.debate', 'view debates (draw)'), ('view.debate.admin', 'view debates (detailed draw)'), ('generate.debate', 'generate debates (draw)'), ('edit.debateteam', 'edit debate teams (pairings)'), ('view.debateadjudicator', 'view debate adjudicators (allocations)'), ('edit.debateadjudicator', 'edit debate adjudicators (allocations)'), ('view.ballotsubmission.new', 'view confirmed ballots'), ('edit.ballotsubmission.old', 'edit non-confirmed ballots'), ('view.ballotsubmission', 'view any ballot'), ('edit.ballotsubmission', 'edit any ballot'), ('add.ballotsubmission', 'create ballots'), ('mark.ballotsubmission', 'confirm/discard any ballot'), ('mark.ballotsubmission.others', "confirm/discard others' ballots"), ('view.ballotsubmission.graph', 'view ballot graph'), ('view.tournamentpreferencemodel', 'view tournament configuration'), ('edit.tournamentpreferencemodel', 'edit tournament configuration')], max_length=50), blank=True, default=list, size=None, verbose_name='permissions')), + ('tournament', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tournaments.tournament', verbose_name='tournament')), + ], + options={ + 'verbose_name': 'group', + 'verbose_name_plural': 'groups', + 'unique_together': {('name', 'tournament')}, + }, + ), migrations.CreateModel( name='UserPermission', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('permission', models.CharField(choices=[('view.adjudicatorteamconflict', 'view adjudicator-team conflicts'), ('edit.adjudicatorteamconflict', 'edit adjudicator-team conflicts'), ('view.adjudicatoradjudicatorconflict', 'view adjudicator-adjudicator conflicts'), ('edit.adjudicatoradjudicatorconflict', 'edit adjudicator-adjudicator conflicts'), ('view.adjudicatorinstitutionconflict', 'view adjudicator-institution conflicts'), ('edit.adjudicatorinstitutionconflict', 'edit adjudicator-institution conflicts'), ('view.teaminstitutionconflict', 'view team-institution conflicts'), ('edit.teaminstitutionconflict', 'edit team-institution conflicts')], verbose_name='permission')), + ('permission', models.CharField(choices=[('view.adjudicatorteamconflict', 'view adjudicator-team conflicts'), ('edit.adjudicatorteamconflict', 'edit adjudicator-team conflicts'), ('view.adjudicatoradjudicatorconflict', 'view adjudicator-adjudicator conflicts'), ('edit.adjudicatoradjudicatorconflict', 'edit adjudicator-adjudicator conflicts'), ('view.adjudicatorinstitutionconflict', 'view adjudicator-institution conflicts'), ('edit.adjudicatorinstitutionconflict', 'edit adjudicator-institution conflicts'), ('view.teaminstitutionconflict', 'view team-institution conflicts'), ('edit.teaminstitutionconflict', 'edit team-institution conflicts'), ('view.actionlogentry', 'view action log entries'), ('view.team', 'view teams'), ('add.team', 'add teams'), ('view.roundavailability.team', 'view round availabilities for teams'), ('view.roundavailability.adjudicator', 'view round availabilities for adjudicators'), ('view.roundavailability.venue', 'view round availabilities for rooms'), ('edit.roundavailability.team', 'edit round availabilities for teams'), ('edit.roundavailability.adjudicator', 'edit round availabilities for adjudicators'), ('edit.roundavailability.venue', 'edit round availabilities for rooms'), ('view.debate', 'view debates (draw)'), ('view.debate.admin', 'view debates (detailed draw)'), ('generate.debate', 'generate debates (draw)'), ('edit.debateteam', 'edit debate teams (pairings)'), ('view.debateadjudicator', 'view debate adjudicators (allocations)'), ('edit.debateadjudicator', 'edit debate adjudicators (allocations)'), ('view.ballotsubmission.new', 'view confirmed ballots'), ('edit.ballotsubmission.old', 'edit non-confirmed ballots'), ('view.ballotsubmission', 'view any ballot'), ('edit.ballotsubmission', 'edit any ballot'), ('add.ballotsubmission', 'create ballots'), ('mark.ballotsubmission', 'confirm/discard any ballot'), ('mark.ballotsubmission.others', "confirm/discard others' ballots"), ('view.ballotsubmission.graph', 'view ballot graph'), ('view.tournamentpreferencemodel', 'view tournament configuration'), ('edit.tournamentpreferencemodel', 'edit tournament configuration')], max_length=50, verbose_name='permission')), ('tournament', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tournaments.tournament', verbose_name='tournament')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')), ], @@ -29,4 +44,17 @@ class Migration(migrations.Migration): 'unique_together': {('user', 'permission', 'tournament')}, }, ), + migrations.CreateModel( + name='Membership', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.group', verbose_name='group')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')), + ], + options={ + 'verbose_name': 'group membership', + 'verbose_name_plural': 'group memberships', + 'unique_together': {('user', 'group')}, + }, + ), ] diff --git a/tabbycat/users/models.py b/tabbycat/users/models.py index c49691cb612..acd1f85e00b 100644 --- a/tabbycat/users/models.py +++ b/tabbycat/users/models.py @@ -2,6 +2,8 @@ from django.db import models from django.utils.translation import gettext_lazy as _ +from utils.fields import ChoiceArrayField + from .permissions import Permission @@ -17,3 +19,28 @@ class Meta: def __str__(self): return "%s: %s (%s)" % (self.user.username, self.permission, self.tournament.slug) + + +class Group(models.Model): + name = models.CharField(max_length=100, verbose_name=_("name")) + tournament = models.ForeignKey('tournaments.Tournament', models.CASCADE, verbose_name=_("tournament")) + permissions = ChoiceArrayField(blank=True, default=list, + base_field=models.CharField(max_length=50, choices=Permission.choices), verbose_name=_("permissions")) + + class Meta: + verbose_name = _("group") + verbose_name_plural = _("groups") + unique_together = [('name', 'tournament')] + + def __str__(self): + return "%s (%s)" % (self.name, self.tournament.slug) + + +class Membership(models.Model): + user = models.ForeignKey(settings.AUTH_USER_MODEL, models.CASCADE, verbose_name=_("user")) + group = models.ForeignKey(Group, models.CASCADE, verbose_name=_("group")) + + class Meta: + verbose_name = _("group membership") + verbose_name_plural = _("group memberships") + unique_together = [('user', 'group')] From 2ff095e483af62de132385be35151b2ed5b0a582 Mon Sep 17 00:00:00 2001 From: Valerie Pang Date: Mon, 28 Aug 2023 16:45:31 -0300 Subject: [PATCH 05/27] Add extra permissions --- tabbycat/users/permissions.py | 53 +++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tabbycat/users/permissions.py b/tabbycat/users/permissions.py index afc7c08b446..6bff88155f8 100644 --- a/tabbycat/users/permissions.py +++ b/tabbycat/users/permissions.py @@ -60,6 +60,9 @@ class Permission(TextChoices): EDIT_DEBATETEAMS = 'edit.debateteam', _("edit debate teams (pairings)") VIEW_DEBATEADJUDICATORS = 'view.debateadjudicator', _("view debate adjudicators (allocations)") EDIT_DEBATEADJUDICATORS = 'edit.debateadjudicator', _("edit debate adjudicators (allocations)") + VIEW_ROOMALLOCATIONS = 'view.roomallocations', _("view room allocations") + EDIT_ROOMALLOCATIONS = 'edit.roomallocations', _("edit room allocations") + EDIT_ALLOCATESIDES = 'edit.allocatesides', _("edit and confirm outround team positions") # Logic behind the ballotsub permissions: # Confirmed ballots are more prominent than old ones, but are more sensitive to changes. @@ -73,5 +76,55 @@ class Permission(TextChoices): MARK_OTHERS_BALLOTSUBMISSIONS = 'mark.ballotsubmission.others', _("confirm/discard others' ballots") VIEW_BALLOTSUBMISSION_GRAPH = 'view.ballotsubmission.graph', _("view ballot graph") + VIEW_MOTION = 'view.roundmotion', _("view motion per round") + EDIT_MOTION = 'edit.roundmotion', _("edit motion per round") + EDIT_RELEASEDRAW = 'edit.releasedraw', _("release draw to public") + EDIT_RELEASEMOTION = 'edit.releasemotion', _("release motion to public") + EDIT_STARTTIME = 'edit.starttime', _("add debate start time") + # these permissions are already assigned to the assistants + VIEW_ALLCURRENTDRAWS_ROOM = 'view.allcurrentdrawsroom', _("view all current draws by room") + VIEW_ALLCURRENTDRAWS_TEAM = 'view.allcurrentdrawsteam', _("view all current draws by team") + VIEW_ROUNDDRAW_ROOM = 'view.rounddrawroom', _("view current round draw by room") + VIEW_ROUNDDRAW_TEAM = 'view.rounddrawteam', _("view current round draw by team") + VIEW_DISPLAYMOTION = 'view.displaymotion', _("view display room motion page") + VIEW_TOURNAMENTPREFERENCEMODEL = 'view.tournamentpreferencemodel', _("view tournament configuration") EDIT_TOURNAMENTPREFERENCEMODEL = 'edit.tournamentpreferencemodel', _("edit tournament configuration") + + VIEW_PREFORMEDPANELS = 'view.preformedpanels', _("view existing preformed panels") + EDIT_PREFORMEDPANELS = 'edit.preformedpanels', _("edit existing preformed panels") + + # standings tab + VIEW_STANDINGS_OVERVIEW = 'view.standingsoverview', _("view the overviews of standings") + VIEW_TEAMSTANDINGS = 'view.teamstandings', _("view the most recent team standings") + VIEW_SPEAKERSSTANDINGS = 'view.speakersstandings', _("view the most recent speaker standings") + VIEW_REPLIESSTANDINGS = 'view.repliesstandings', _("view the most recent replies standings") + VIEW_MOTIONSTAB = 'view.motionstab', _("view the most recent motions tab") + VIEW_DIVERSITYTAB = 'view.diversitytab', _("view the diversity tab") + + # Feedback tab + VIEW_FEEDBACK_OVERVIEW = 'view.feedbackoverview', _("view overview of judge feedback scores") + EDIT_FEEDBACK_OVERVIEW = 'edit.feedbackoverview', _("edit overview of judge feedback scores") + # not sure what the right most column of the overview page is called, but I'm calling it comments for now + EDIT_JUDGESCORES_BULK = 'edit.judgescoresbulk', _("bulk update judge scores") + EDIT_BASEJUDGESCORES_IND = 'edit.judgescoresind', _("edit base scores of judges") + EDIT_SETBREAKING = 'edit.setbreaking', _("edit breaking judges") + VIEW_FEEDBACK_LATEST = 'view.feedbacklatest', _("view the latest feedback tab") + VIEW_FEEDBACK_IMPORTANT = 'view.feedbackimportant', _("view the important feedback tab") + VIEW_FEEDBACK_COMMENTS = 'view.feedbackcomments', _("view the comments feedback tab") + VIEW_FEEDBACK_BYSOURCE = 'view.feedbackbysource', _("view feedback by source") + VIEW_FEEDBACK_BYTARGET = 'view.feedbackbytarget', _("view feedback by target") + EDIT_FEEDBACK_IGNORE = 'edit.feedbackignore', _("edit the ignore feedback feature") + EDIT_FEEDBACK_UNCONFIRM = 'edit.feedbackunconfirm', _("edit the unconfirm feedback feature") + VIEW_FEEDBACK_UNSUBMITTED = 'view.feedbackunsubmitted', _("view feedback unsubmitted tab") + VIEW_FEEDBACK_ADD = 'view.feedbackadd', _("view add feedback tab") + EDIT_FEEDBACK_ADD = 'edit.feedbackadd', _("edit add feedback tab") + # idk if its possible for them to add feedback everywhere, considering there is add feedback on multiple pages + + # breaks + EDIT_BREAK_ELIGIBILITY = 'edit.breakeligibility', _("edit break eligibility") + VIEW_BREAK_ELIGIBILITY = 'view.breakeligibility', _("view break eligibility") + EDIT_BREAK_CATEGORIES = 'edit.breakcategories', _("edit break categories") + VIEW_BREAK_CATEGORIES = 'view.breakcategories', _("view break categories") + GENERATE_BREAK = 'generate.break', _("generate all breaks") + EDIT_BREAK_REMARKS = 'edit.breakremarks', _("edit break remarks") From 42e542906b380336bc66669a0346f560ff53a183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Sun, 10 Sep 2023 15:32:45 -0300 Subject: [PATCH 06/27] Associate permissions with views This commit sets the newly-added permissions with the views that they'll restrict. It also makes some small modifications to the enumeration. We also set the view permission to be the edit permission if it is set, for views which are useless if cannot post (like adding feedback). It also adds simple typings and fixes a bug where the user object's set of permissions could only be size of 1. --- tabbycat/adjfeedback/views.py | 13 ++++++ tabbycat/breakqual/views.py | 11 +++++ tabbycat/users/groups.py | 6 ++- tabbycat/users/permissions.py | 81 +++++++++++++++++++---------------- tabbycat/utils/mixins.py | 16 ++++--- 5 files changed, 82 insertions(+), 45 deletions(-) diff --git a/tabbycat/adjfeedback/views.py b/tabbycat/adjfeedback/views.py index 99b781ca194..e9617a21553 100644 --- a/tabbycat/adjfeedback/views.py +++ b/tabbycat/adjfeedback/views.py @@ -23,6 +23,7 @@ from tournaments.mixins import (PersonalizablePublicTournamentPageMixin, PublicTournamentPageMixin, SingleObjectByRandomisedUrlMixin, SingleObjectFromTournamentMixin, TournamentMixin) from tournaments.models import Round +from users.permissions import Permission from utils.misc import reverse_tournament from utils.mixins import AdministratorMixin, AssistantMixin from utils.tables import TabbycatTableBuilder @@ -122,6 +123,7 @@ class FeedbackOverview(AdministratorMixin, BaseFeedbackOverview): sort_key = 'score' sort_order = 'desc' template_name = 'feedback_overview.html' + view_permission = Permission.VIEW_FEEDBACK_OVERVIEW def annotate_table(self, table, adjudicators): feedback_weight = self.tournament.current_round.feedback_weight @@ -142,6 +144,7 @@ class FeedbackByTargetView(AdministratorMixin, TournamentMixin, VueTableTemplate template_name = "feedback_base.html" page_title = gettext_lazy("Find Feedback on Adjudicator") page_emoji = '๐Ÿ”' + view_permission = Permission.VIEW_FEEDBACK def get_table(self): adjudicators = self.tournament.adjudicator_set.annotate(feedback_count=Count('adjudicatorfeedback')) @@ -164,6 +167,7 @@ class FeedbackBySourceView(AdministratorMixin, TournamentMixin, VueTableTemplate template_name = "feedback_base.html" page_title = gettext_lazy("Find Feedback") page_emoji = '๐Ÿ”' + view_permission = Permission.VIEW_FEEDBACK def get_tables(self): tournament = self.tournament @@ -252,6 +256,7 @@ def get_feedback_queryset(self): class FeedbackCardsView(FeedbackMixin, AdministratorMixin, TournamentMixin, TemplateView): """Base class for views displaying feedback as cards.""" template_name = "feedback_cards_list.html" + view_permission = Permission.VIEW_FEEDBACK def get_score_thresholds(self): tournament = self.tournament @@ -404,6 +409,7 @@ class AdminAddFeedbackIndexView(AdministratorMixin, BaseAddFeedbackIndexView): of the feedback.""" template_name = 'add_feedback.html' tabroom = True + view_permission = Permission.ADD_FEEDBACK def get_from_adj_link(self, adj): return reverse_tournament('adjfeedback-add-from-adjudicator', @@ -521,6 +527,7 @@ def get_success_url(self): class AdminAddFeedbackView(AdministratorMixin, BaseTabroomAddFeedbackView): + edit_permission = Permission.ADD_FEEDBACK pass @@ -644,6 +651,7 @@ class SetAdjudicatorBaseScoreView(BaseAdjudicatorActionView): action_log_type = ActionLogEntry.ACTION_TYPE_TEST_SCORE_EDIT action_log_content_object_attr = 'atsh' + edit_permission = Permission.EDIT_BASEJUDGESCORES_IND def modify_adjudicator(self, request, adjudicator): try: @@ -664,6 +672,7 @@ def modify_adjudicator(self, request, adjudicator): class SetAdjudicatorBreakingStatusView(AdministratorMixin, TournamentMixin, LogActionMixin, View): action_log_type = ActionLogEntry.ACTION_TYPE_ADJUDICATOR_BREAK_SET + edit_permission = Permission.EDIT_SETBREAKING def post(self, request, *args, **kwargs): body = self.request.body.decode('utf-8') @@ -722,6 +731,7 @@ def get_tables(self): class FeedbackProgress(AdministratorMixin, BaseFeedbackProgressView): template_name = 'feedback_base.html' + view_permission = Permission.VIEW_FEEDBACK_UNSUBMITTED class PublicFeedbackProgress(PublicTournamentPageMixin, BaseFeedbackProgressView): @@ -754,6 +764,7 @@ def get_redirect_url(self, *args, **kwargs): class ConfirmFeedbackView(BaseFeedbackToggleView): + edit_permission = Permission.EDIT_FEEDBACK_CONFIRM def feedback_result(self, feedback): return _("confirmed") if feedback.confirmed else _("un-confirmed") @@ -767,6 +778,7 @@ def modify_feedback(self, feedback): class IgnoreFeedbackView(BaseFeedbackToggleView): + edit_permission = Permission.EDIT_FEEDBACK_IGNORE def feedback_result(self, feedback): return _("ignored") if feedback.ignored else _("un-ignored") @@ -784,6 +796,7 @@ class UpdateAdjudicatorScoresView(AdministratorMixin, LogActionMixin, Tournament template_name = 'update_adjudicator_scores.html' form_class = UpdateAdjudicatorScoresForm action_log_type = ActionLogEntry.ACTION_TYPE_UPDATE_ADJUDICATOR_SCORES + edit_permission = Permission.EDIT_JUDGESCORES_BULK def get_context_data(self, **kwargs): sample_adjs = self.tournament.relevant_adjudicators.all()[:3] diff --git a/tabbycat/breakqual/views.py b/tabbycat/breakqual/views.py index b775a53757b..9464b8bdfff 100644 --- a/tabbycat/breakqual/views.py +++ b/tabbycat/breakqual/views.py @@ -16,6 +16,7 @@ from participants.models import Team from participants.views import EditSpeakerCategoriesView, UpdateEligibilityEditView as BaseUpdateEligibilityEditView from tournaments.mixins import PublicTournamentPageMixin, SingleObjectFromTournamentMixin, TournamentMixin +from users.permissions import Permission from utils.misc import reverse_tournament from utils.mixins import AdministratorMixin from utils.tables import TabbycatTableBuilder @@ -39,6 +40,7 @@ class PublicBreakIndexView(PublicTournamentPageMixin, TemplateView): class AdminBreakIndexView(AdministratorMixin, TournamentMixin, TemplateView): template_name = 'breaking_index.html' + view_permission = Permission.VIEW_BREAK_OVERVIEW def get_context_data(self, **kwargs): tournament = self.tournament @@ -109,6 +111,8 @@ class BreakingTeamsFormView(GenerateBreakMixin, LogActionMixin, AdministratorMix form_class = forms.BreakingTeamsForm template_name = 'breaking_teams.html' action_log_content_object_attr = 'object' + view_permission = Permission.VIEW_BREAK + edit_permission = Permission.GENERATE_BREAK def get_action_log_type(self): if 'save_update_all' in self.request.POST: @@ -188,6 +192,7 @@ class GenerateAllBreaksView(GenerateBreakMixin, LogActionMixin, TournamentMixin, action_log_type = ActionLogEntry.ACTION_TYPE_BREAK_GENERATE_ALL tournament_redirect_pattern_name = 'breakqual-teams' + edit_permission = Permission.GENERATE_BREAK def post(self, request, *args, **kwargs): BreakingTeam.objects.filter(break_category__tournament=self.tournament).delete() @@ -219,6 +224,7 @@ def get_table(self): class AdminBreakingAdjudicatorsView(AdministratorMixin, BaseBreakingAdjudicatorsView): template_name = 'breaking_adjs.html' + view_permission = Permission.VIEW_ADJ_BREAK class PublicBreakingAdjudicatorsView(PublicTournamentPageMixin, BaseBreakingAdjudicatorsView): @@ -254,6 +260,8 @@ class EditBreakCategoriesView(EditSpeakerCategoriesView): template_name = 'break_categories_edit.html' formset_model = BreakCategory action_log_type = ActionLogEntry.ACTION_TYPE_BREAK_CATEGORIES_EDIT + view_permission = Permission.VIEW_BREAK_CATEGORIES + edit_permission = Permission.EDIT_BREAK_CATEGORIES url_name = 'break-categories-edit' success_url = 'breakqual-index' @@ -280,6 +288,8 @@ class EditTeamEligibilityView(AdministratorMixin, TournamentMixin, VueTableTempl template_name = 'edit_break_eligibility.html' page_title = _("Break Eligibility") page_emoji = '๐Ÿฏ' + view_permission = Permission.VIEW_BREAK_ELIGIBILITY + edit_permission = Permission.EDIT_BREAK_ELIGIBILITY def get_table(self): t = self.tournament @@ -331,3 +341,4 @@ class UpdateEligibilityEditView(BaseUpdateEligibilityEditView): action_log_type = ActionLogEntry.ACTION_TYPE_BREAK_ELIGIBILITY_EDIT participant_model = Team many_to_many_field = 'break_categories' + edit_permission = Permission.EDIT_BREAK_ELIGIBILITY diff --git a/tabbycat/users/groups.py b/tabbycat/users/groups.py index 7619ace909a..aeee7a0051a 100644 --- a/tabbycat/users/groups.py +++ b/tabbycat/users/groups.py @@ -1,3 +1,5 @@ +from typing import List, Optional + from django.utils.translation import gettext_lazy as _ from options.presets import _all_subclasses @@ -10,8 +12,8 @@ def all_groups(): class BaseGroup: - name = None - permissions = [] + name: Optional[str] = None + permissions: List[Permission] = [] class Equity(BaseGroup): diff --git a/tabbycat/users/permissions.py b/tabbycat/users/permissions.py index 6bff88155f8..0d6a1e2ab46 100644 --- a/tabbycat/users/permissions.py +++ b/tabbycat/users/permissions.py @@ -1,34 +1,12 @@ +from typing import TYPE_CHECKING, Union + from django.core.cache import cache from django.db.models import TextChoices from django.utils.translation import gettext_lazy as _ - -def has_permission(user, permission, tournament): - if user.is_superuser: - return True - - if isinstance(permission, bool): - return permission - - if not hasattr(user, '_permissions'): - user._permissions = {} - - if tournament.slug in user._permissions: - return permission in user._permissions[tournament.slug] - else: - user._permissions[tournament.slug] = set() - - cached_perm = cache.get("user_%d_%s_%s_permission" % (user.pk, tournament.slug, str(permission))) - if cached_perm is not None: - if cached_perm: - user._permissions[tournament.slug].add(permission) - return cached_perm - - perm = user.userpermission_set.filter(permission=permission, tournament=tournament).exists() - cache.set("user_%d_%s_%s_permission" % (user.pk, tournament.slug, str(permission)), perm) - if perm: - user._permissions[tournament.slug].add(permission) - return perm +if TYPE_CHECKING: + from django.conf import settings + from tournaments.models import Tournament class Permission(TextChoices): @@ -109,16 +87,12 @@ class Permission(TextChoices): EDIT_JUDGESCORES_BULK = 'edit.judgescoresbulk', _("bulk update judge scores") EDIT_BASEJUDGESCORES_IND = 'edit.judgescoresind', _("edit base scores of judges") EDIT_SETBREAKING = 'edit.setbreaking', _("edit breaking judges") - VIEW_FEEDBACK_LATEST = 'view.feedbacklatest', _("view the latest feedback tab") - VIEW_FEEDBACK_IMPORTANT = 'view.feedbackimportant', _("view the important feedback tab") - VIEW_FEEDBACK_COMMENTS = 'view.feedbackcomments', _("view the comments feedback tab") - VIEW_FEEDBACK_BYSOURCE = 'view.feedbackbysource', _("view feedback by source") - VIEW_FEEDBACK_BYTARGET = 'view.feedbackbytarget', _("view feedback by target") - EDIT_FEEDBACK_IGNORE = 'edit.feedbackignore', _("edit the ignore feedback feature") - EDIT_FEEDBACK_UNCONFIRM = 'edit.feedbackunconfirm', _("edit the unconfirm feedback feature") + VIEW_FEEDBACK = 'view.feedback', _("view feedback") + EDIT_FEEDBACK_IGNORE = 'edit.feedbackignore', _("toggle ignore feedback") + EDIT_FEEDBACK_CONFIRM = 'edit.feedbackconfirm', _("toggle confirm feedback") VIEW_FEEDBACK_UNSUBMITTED = 'view.feedbackunsubmitted', _("view feedback unsubmitted tab") - VIEW_FEEDBACK_ADD = 'view.feedbackadd', _("view add feedback tab") - EDIT_FEEDBACK_ADD = 'edit.feedbackadd', _("edit add feedback tab") + ADD_FEEDBACK = 'add.feedback', _("add feedback") + VIEW_ADJ_BREAK = 'view.adj.break', _("view adjudicator break") # idk if its possible for them to add feedback everywhere, considering there is add feedback on multiple pages # breaks @@ -126,5 +100,38 @@ class Permission(TextChoices): VIEW_BREAK_ELIGIBILITY = 'view.breakeligibility', _("view break eligibility") EDIT_BREAK_CATEGORIES = 'edit.breakcategories', _("edit break categories") VIEW_BREAK_CATEGORIES = 'view.breakcategories', _("view break categories") + VIEW_BREAK_OVERVIEW = 'view.break.overview', _("view break overview") + VIEW_BREAK = 'view.break', _("view breaks") GENERATE_BREAK = 'generate.break', _("generate all breaks") - EDIT_BREAK_REMARKS = 'edit.breakremarks', _("edit break remarks") + + +permission_type = Union[Permission, bool] + + +def has_permission(user: 'settings.AUTH_USER_MODEL', permission: permission_type, tournament: 'Tournament') -> bool: + if user.is_superuser: + return True + + if isinstance(permission, bool): + return permission + + if not hasattr(user, '_permissions'): + user._permissions = {} + + if tournament.slug in user._permissions: + if permission in user._permissions[tournament.slug]: + return True + else: + user._permissions[tournament.slug] = set() + + cached_perm = cache.get("user_%d_%s_%s_permission" % (user.pk, tournament.slug, str(permission))) + if cached_perm is not None: + if cached_perm: + user._permissions[tournament.slug].add(permission) + return cached_perm + + perm = user.userpermission_set.filter(permission=permission, tournament=tournament).exists() + cache.set("user_%d_%s_%s_permission" % (user.pk, tournament.slug, str(permission)), perm) + if perm: + user._permissions[tournament.slug].add(permission) + return perm diff --git a/tabbycat/utils/mixins.py b/tabbycat/utils/mixins.py index 4268fd3e733..dcc41f669f4 100644 --- a/tabbycat/utils/mixins.py +++ b/tabbycat/utils/mixins.py @@ -1,5 +1,6 @@ import logging import os +from typing import Optional, TYPE_CHECKING from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin @@ -10,6 +11,9 @@ from users.permissions import has_permission +if TYPE_CHECKING: + from users.permissions import permission_type + logger = logging.getLogger(__name__) @@ -53,20 +57,20 @@ class AdministratorMixin(UserPassesTestMixin, ContextMixin): Requires user to be a superuser.""" view_role = "admin" for_admin = True - view_permission = None - edit_permission = None + view_permission: Optional['permission_type'] = None + edit_permission: Optional['permission_type'] = None def get_context_data(self, **kwargs): kwargs["user_role"] = self.view_role return super().get_context_data(**kwargs) - def get_view_permission(self): - return self.view_permission + def get_view_permission(self) -> Optional['permission_type']: + return self.view_permission or self.edit_permission - def get_edit_permission(self): + def get_edit_permission(self) -> Optional['permission_type']: return self.edit_permission - def test_func(self): + def test_func(self) -> bool: if self.request.method == 'GET' and self.get_view_permission() is not None: return has_permission(self.request.user, self.get_view_permission(), self.tournament) if self.request.method == 'POST' and self.get_edit_permission() is not None: From 617fdb953bf2102451fe61f7b1b4d230d1d1a498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Sat, 18 Nov 2023 12:12:47 -0400 Subject: [PATCH 07/27] API: Add permissions to User endpoints --- tabbycat/api/serializers.py | 27 +++++++++++++++++++++++---- tabbycat/api/urls.py | 9 +++++---- tabbycat/api/views.py | 25 +++++++++++++++++++++---- tabbycat/users/permissions.py | 21 ++++++++++++++++++++- 4 files changed, 69 insertions(+), 13 deletions(-) diff --git a/tabbycat/api/serializers.py b/tabbycat/api/serializers.py index 4b9ba5f7aa4..c0f6aa83944 100644 --- a/tabbycat/api/serializers.py +++ b/tabbycat/api/serializers.py @@ -27,6 +27,8 @@ from standings.speakers import SpeakerStandingsGenerator from standings.teams import TeamStandingsGenerator from tournaments.models import Round, Tournament +from users.models import Group +from users.permissions import Permission from venues.models import Venue, VenueCategory, VenueConstraint from . import fields @@ -56,7 +58,7 @@ class V1RootSerializer(serializers.Serializer): class V1LinksSerializer(serializers.Serializer): tournaments = serializers.HyperlinkedIdentityField(view_name='api-tournament-list') institutions = serializers.HyperlinkedIdentityField(view_name='api-global-institution-list') - users = serializers.HyperlinkedIdentityField(view_name='api-users-list') + users = serializers.HyperlinkedIdentityField(view_name='api-user-list') _links = V1LinksSerializer(source='*', read_only=True) @@ -157,7 +159,12 @@ def create(self, validated_data): if isinstance(motion_data, Motion): # If passed in a URL - Becomes an object validated_data['motion'] = motion_data else: - validated_data['motion'] = Motion(text=motion_data['text'], reference=motion_data['reference'], info_slide=motion_data.get('info_slide', ''), tournament=self.context['tournament']) + validated_data['motion'] = Motion( + text=motion_data['text'], + reference=motion_data['reference'], + info_slide=motion_data.get('info_slide', ''), + tournament=self.context['tournament'], + ) validated_data['motion'].save() return super().create(validated_data) @@ -1397,11 +1404,23 @@ class Meta: class UserSerializer(serializers.ModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='api-users-detail') + class TournamentPermissionsSerializer(serializers.Serializer): + class UserGroupSerializer(serializers.ModelSerializer): + class Meta: + model = Group + fields = ('name', 'permissions') + + tournament = serializers.HyperlinkedIdentityField(view_name='api-tournament-detail') + groups = UserGroupSerializer(many=True, required=False) + permissions = serializers.ListField(child=serializers.ChoiceField(choices=Permission.choices), required=False) + + url = serializers.HyperlinkedIdentityField(view_name='api-user-detail') + tournaments = TournamentPermissionsSerializer(many=True, required=False) class Meta: model = get_user_model() - fields = ('url', 'id', 'username', 'password', 'email', 'is_staff', 'is_superuser', 'is_active') + fields = ('id', 'url', 'username', 'password', 'is_superuser', 'is_staff', 'email', 'is_active', 'date_joined', 'last_login', 'tournaments') + read_only_fields = ('date_joined', 'last_login') def create(self, validated_data): user = self.Meta.model(**validated_data) diff --git a/tabbycat/api/urls.py b/tabbycat/api/urls.py index 5fc195e9e0b..c651b4bb5cb 100644 --- a/tabbycat/api/urls.py +++ b/tabbycat/api/urls.py @@ -236,13 +236,14 @@ views.GlobalInstitutionViewSet.as_view(detail_methods), name='api-global-institution-detail'), ])), + path('/users', include([ path('', - views.UserViewSet.as_view(list_methods), - name='api-users-list'), + views.UsersViewSet.as_view(list_methods), + name='api-user-list'), path('/', - views.UserViewSet.as_view({'get': 'retrieve', 'delete': 'destroy'}), - name='api-users-detail'), + views.UsersViewSet.as_view(detail_methods), + name='api-user-detail'), ])), ])), ] diff --git a/tabbycat/api/views.py b/tabbycat/api/views.py index 20319d2dfff..1519541668f 100644 --- a/tabbycat/api/views.py +++ b/tabbycat/api/views.py @@ -4,6 +4,7 @@ from asgiref.sync import async_to_sync from channels.layers import get_channel_layer from django.conf import settings +from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.db.models import Count, Prefetch, Q from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter @@ -35,6 +36,7 @@ from standings.teams import TeamStandingsGenerator from tournaments.mixins import TournamentFromUrlMixin from tournaments.models import Round, Tournament +from users.permissions import get_permissions from venues.models import Venue, VenueCategory from . import serializers @@ -82,7 +84,7 @@ def get(self, request, format=None): """Entrypoint for version 1 of the API""" tournaments_create_url = reverse('api-tournament-list', request=request, format=format) institution_create_url = reverse('api-global-institution-list', request=request, format=format) - users_create_url = reverse('api-users-list', request=request, format=format) + users_create_url = reverse('api-user-list', request=request, format=format) return Response({ "_links": { "tournaments": tournaments_create_url, @@ -1097,12 +1099,27 @@ def add_blank(self, request, *args, **kwargs): @extend_schema(tags=['users']) @extend_schema_view( - list=extend_schema(summary="Get users"), + list=extend_schema(summary="List all users"), create=extend_schema(summary="Create user"), retrieve=extend_schema(summary="Get user", parameters=[id_parameter]), + update=extend_schema(summary="Update user", parameters=[id_parameter]), + partial_update=extend_schema(summary="Patch user", parameters=[id_parameter]), + destroy=extend_schema(summary="Deactivate user", parameters=[id_parameter]), ) -class UserViewSet(AdministratorAPIMixin, ModelViewSet): +class UsersViewSet(AdministratorAPIMixin, ModelViewSet): serializer_class = serializers.UserSerializer def get_queryset(self): - return self.get_serializer_class().Meta.model.objects.all() + qs = get_user_model().objects.prefetch_related('membership_set__group__tournament', 'userpermission_set__tournament') + for user in qs: + user.tournaments = get_permissions(user) + return qs + + def get_object(self): + obj = super().get_object() + obj.tournaments = get_permissions(obj) + return obj + + def perform_destroy(self, instance): + instance.is_active = False + instance.save() diff --git a/tabbycat/users/permissions.py b/tabbycat/users/permissions.py index 0d6a1e2ab46..3691f7b85f8 100644 --- a/tabbycat/users/permissions.py +++ b/tabbycat/users/permissions.py @@ -1,3 +1,4 @@ +from itertools import groupby from typing import TYPE_CHECKING, Union from django.core.cache import cache @@ -130,8 +131,26 @@ def has_permission(user: 'settings.AUTH_USER_MODEL', permission: permission_type user._permissions[tournament.slug].add(permission) return cached_perm - perm = user.userpermission_set.filter(permission=permission, tournament=tournament).exists() + perm = ( + user.userpermission_set.filter(permission=permission, tournament=tournament).exists() or + user.membership_set.filter(group__permissions__contains=[permission]).exists() + ) cache.set("user_%d_%s_%s_permission" % (user.pk, tournament.slug, str(permission)), perm) if perm: user._permissions[tournament.slug].add(permission) return perm + + +def get_permissions(user: 'settings.AUTH_USER_MODEL') -> dict: + groups = dict(groupby(user.membership_set.select_related('group', 'group__tournament').order_by('group__tournament').all(), key=lambda m: m.group.tournament)) + permissions = dict(groupby(user.userpermission_set.select_related('tournament').order_by('tournament').all(), key=lambda p: p.tournament)) + + user_perms = [] + for tournament in set(list(groups.keys()) + list(permissions.keys())): + user_perms.append({ + 'tournament': tournament, + 'groups': [m.group for m in groups.get(tournament, [])], + 'permissions': [p.permission for p in permissions.get(tournament, [])], + }) + + return user_perms From ac5ba344b3ff501debbb8f17647693431e9b8914 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Sat, 18 Nov 2023 13:17:48 -0400 Subject: [PATCH 08/27] Add date to round start time This commit migrates the Round "start time" field to include the date (and timezone) in order to better support prepared rounds. The generated migration will take the site's timezone and the first ballot that was submitted to create a DateTime. The "set start time" modal now has an initial value of 15 minutes after the current date-time, and the date is shown. The API will accept both time and datetime objects. --- tabbycat/api/serializers.py | 12 +++++++ tabbycat/api/tests/test_serializers.py | 34 +++++++++++++++++++ .../draw/templates/draw_display_admin.html | 2 +- .../templates/current_round/common.html | 2 +- .../migrations/0011_alter_round_starts_at.py | 31 +++++++++++++++++ tabbycat/tournaments/models.py | 2 +- tabbycat/utils/templatetags/debate_tags.py | 6 ++++ 7 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 tabbycat/tournaments/migrations/0011_alter_round_starts_at.py diff --git a/tabbycat/api/serializers.py b/tabbycat/api/serializers.py index 4b9ba5f7aa4..edd430f2f46 100644 --- a/tabbycat/api/serializers.py +++ b/tabbycat/api/serializers.py @@ -1,5 +1,6 @@ from collections import OrderedDict from collections.abc import Mapping +from datetime import date, datetime, time from functools import partialmethod from django.contrib.auth import get_user_model @@ -167,6 +168,16 @@ class RoundLinksSerializer(serializers.Serializer): view_name='api-pairing-list', lookup_field='seq', lookup_url_kwarg='round_seq') + class TimeOrDateTimeField(serializers.DateTimeField): + def to_internal_value(self, value): + try: + value = time.fromisoformat(value) + except ValueError: + return super().to_internal_value(value) + + value = datetime.combine(date.today(), value) + return super().to_internal_value(value) + url = fields.TournamentHyperlinkedIdentityField( view_name='api-round-detail', lookup_field='seq', lookup_url_kwarg='round_seq') @@ -175,6 +186,7 @@ class RoundLinksSerializer(serializers.Serializer): queryset=BreakCategory.objects.all(), allow_null=True, required=False) motions = RoundMotionSerializer(many=True, source='roundmotion_set', required=False) + starts_at = TimeOrDateTimeField(required=False, allow_null=True) _links = RoundLinksSerializer(source='*', read_only=True) diff --git a/tabbycat/api/tests/test_serializers.py b/tabbycat/api/tests/test_serializers.py index ee8fa7267c4..5f5e73c5ac9 100644 --- a/tabbycat/api/tests/test_serializers.py +++ b/tabbycat/api/tests/test_serializers.py @@ -1,4 +1,6 @@ import logging +import zoneinfo +from datetime import date, datetime, time from django.contrib.auth import get_user_model from rest_framework.test import APIClient, APITestCase @@ -13,6 +15,7 @@ from utils.tests import CompletedTournamentTestMixin User = get_user_model() +tz = zoneinfo.ZoneInfo('Australia/Melbourne') class RoundSerializerTests(CompletedTournamentTestMixin, APITestCase): @@ -102,6 +105,37 @@ def test_can_update_round_motion(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.data['name'], 'Round Five') + def test_can_give_start_datetime(self): + client = APIClient() + client.login(username="admin", password="admin") + self.tournament.round_set.get(seq=5).delete() + response = client.post(reverse_tournament('api-round-list', self.tournament), { + 'motions': [], + 'seq': 5, + 'name': 'Round 5', + 'abbreviation': 'R5', + 'draw_type': 'P', + 'starts_at': '2023-11-18T00:00:00Z', + }) + self.assertEqual(response.status_code, 201) + self.assertEqual(datetime.fromisoformat(response.data['starts_at']), datetime(2023, 11, 18, 11, 0, 0, tzinfo=tz)) + + def test_can_give_start_time(self): + client = APIClient() + client.login(username="admin", password="admin") + self.tournament.round_set.get(seq=5).delete() + response = client.post(reverse_tournament('api-round-list', self.tournament), { + 'motions': [], + 'seq': 5, + 'name': 'Round 5', + 'abbreviation': 'R5', + 'draw_type': 'P', + 'starts_at': '00:00:00', + }) + print(response.data) + self.assertEqual(response.status_code, 201) + self.assertEqual(datetime.fromisoformat(response.data['starts_at']), datetime.combine(date.today(), time(0, 0, 0, tzinfo=tz))) + class MotionSerializerTests(CompletedTournamentTestMixin, APITestCase): diff --git a/tabbycat/draw/templates/draw_display_admin.html b/tabbycat/draw/templates/draw_display_admin.html index 7904f726021..936c8524ed6 100644 --- a/tabbycat/draw/templates/draw_display_admin.html +++ b/tabbycat/draw/templates/draw_display_admin.html @@ -357,7 +357,7 @@
      - +
      diff --git a/tabbycat/participants/templates/current_round/common.html b/tabbycat/participants/templates/current_round/common.html index 4a847884f06..bf4c962cbcd 100644 --- a/tabbycat/participants/templates/current_round/common.html +++ b/tabbycat/participants/templates/current_round/common.html @@ -3,7 +3,7 @@ {# Round start time #} {% if current_round.starts_at %}
      - {% blocktrans trimmed with start_time=current_round.starts_at %} + {% blocktrans trimmed with start_time=current_round.starts_at|time:'H:i' %} The round begins at {{ start_time }} {% endblocktrans %}
      diff --git a/tabbycat/tournaments/migrations/0011_alter_round_starts_at.py b/tabbycat/tournaments/migrations/0011_alter_round_starts_at.py new file mode 100644 index 00000000000..0e7e04f35bd --- /dev/null +++ b/tabbycat/tournaments/migrations/0011_alter_round_starts_at.py @@ -0,0 +1,31 @@ +# Generated by Django 4.2.5 on 2023-11-18 16:17 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("tournaments", "0010_alter_round_draw_type"), + ] + + operations = [ + migrations.AddField( + model_name="round", + name="starts_at_", + field=models.DateTimeField(blank=True, null=True, verbose_name="starts at"), + ), + migrations.RunSQL( + "WITH first_ballot AS (SELECT DISTINCT ON (round_id) d.round_id, bs.timestamp FROM results_ballotsubmission bs INNER JOIN draw_debate d ON d.id=bs.debate_id ORDER BY round_id, timestamp) UPDATE tournaments_round r SET starts_at_=fb.timestamp::date + r.starts_at AT TIME ZONE '" + settings.TIME_ZONE + "' FROM first_ballot fb WHERE r.id=fb.round_id;", + migrations.RunSQL.noop, + ), + migrations.RemoveField( + model_name="round", + name="starts_at", + ), + migrations.RenameField( + model_name="round", + old_name="starts_at_", + new_name="starts_at", + ), + ] diff --git a/tabbycat/tournaments/models.py b/tabbycat/tournaments/models.py index 4fe96becab5..3eed62e3719 100644 --- a/tabbycat/tournaments/models.py +++ b/tabbycat/tournaments/models.py @@ -331,7 +331,7 @@ class Status(models.TextChoices): motions_released = models.BooleanField(default=False, verbose_name=_("motions released"), help_text=_("Whether motions will appear on the public website, assuming that feature is turned on")) - starts_at = models.TimeField(verbose_name=_("starts at"), blank=True, null=True) + starts_at = models.DateTimeField(verbose_name=_("starts at"), blank=True, null=True) weight = models.IntegerField(default=1, verbose_name=_("weight"), diff --git a/tabbycat/utils/templatetags/debate_tags.py b/tabbycat/utils/templatetags/debate_tags.py index a774288a6eb..5c8d7de3eac 100644 --- a/tabbycat/utils/templatetags/debate_tags.py +++ b/tabbycat/utils/templatetags/debate_tags.py @@ -1,5 +1,6 @@ import os import re +from datetime import datetime, timedelta from random import randint from django import template @@ -241,3 +242,8 @@ def abbreviatename(name): """Takes a two-part name and returns an abbreviation like 'E.Luฤiฤ‡'.""" parts = name.split(" ") return "%s.%s" % (parts[0][:5], parts[-1][:5]) # Used for barcodes + + +@register.simple_tag +def prep_time(): + return (datetime.now() + timedelta(minutes=15)).strftime('%Y-%m-%dT%H:%M') From 887a20b8a213e668d11558d2d863a710dd6387eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Sat, 9 Dec 2023 16:29:30 -0400 Subject: [PATCH 09/27] Add test for anonymous users --- tabbycat/users/permissions.py | 2 ++ tabbycat/utils/mixins.py | 5 ++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tabbycat/users/permissions.py b/tabbycat/users/permissions.py index 3691f7b85f8..9dd7c832de5 100644 --- a/tabbycat/users/permissions.py +++ b/tabbycat/users/permissions.py @@ -110,6 +110,8 @@ class Permission(TextChoices): def has_permission(user: 'settings.AUTH_USER_MODEL', permission: permission_type, tournament: 'Tournament') -> bool: + if user.is_anonymous: + return False if user.is_superuser: return True diff --git a/tabbycat/utils/mixins.py b/tabbycat/utils/mixins.py index dcc41f669f4..ed4b9585e91 100644 --- a/tabbycat/utils/mixins.py +++ b/tabbycat/utils/mixins.py @@ -53,8 +53,7 @@ def get_context_data(self, **kwargs): # ============================================================================== class AdministratorMixin(UserPassesTestMixin, ContextMixin): - """Mixin for views that are for administrators. - Requires user to be a superuser.""" + """Mixin for views that are for administrators.""" view_role = "admin" for_admin = True view_permission: Optional['permission_type'] = None @@ -73,7 +72,7 @@ def get_edit_permission(self) -> Optional['permission_type']: def test_func(self) -> bool: if self.request.method == 'GET' and self.get_view_permission() is not None: return has_permission(self.request.user, self.get_view_permission(), self.tournament) - if self.request.method == 'POST' and self.get_edit_permission() is not None: + if self.request.method in ['POST', 'PUT'] and self.get_edit_permission() is not None: return has_permission(self.request.user, self.get_edit_permission(), self.tournament) return self.request.user.is_superuser From ac73229664d8c66e6344bc147bf3cc33219c295c Mon Sep 17 00:00:00 2001 From: Tran Trang Linh Date: Sun, 28 Jan 2024 07:14:57 +0700 Subject: [PATCH 10/27] Add extra permissions and link permisions with views (#2390) --- tabbycat/adjallocation/views.py | 24 ++++++++---- tabbycat/adjfeedback/views.py | 2 +- tabbycat/availability/views.py | 13 +++++++ tabbycat/checkins/views.py | 5 +++ tabbycat/draw/views.py | 12 +++++- tabbycat/importer/views.py | 5 +++ tabbycat/motions/views.py | 8 +++- tabbycat/participants/views.py | 6 ++- tabbycat/privateurls/views.py | 6 ++- tabbycat/results/views.py | 2 + tabbycat/standings/views.py | 3 ++ tabbycat/users/groups.py | 69 +++++++++++++++++++++++++++++++-- tabbycat/users/permissions.py | 46 +++++++++++++++------- tabbycat/venues/views.py | 4 ++ 14 files changed, 176 insertions(+), 29 deletions(-) diff --git a/tabbycat/adjallocation/views.py b/tabbycat/adjallocation/views.py index fed70adbd48..a07240704c7 100644 --- a/tabbycat/adjallocation/views.py +++ b/tabbycat/adjallocation/views.py @@ -13,7 +13,7 @@ from participants.models import Adjudicator, Region from participants.prefetch import populate_feedback_scores from tournaments.mixins import DebateDragAndDropMixin, TournamentMixin -from users.permissions import has_permission +from users.permissions import has_permission, Permission from utils.misc import ranks_dictionary, redirect_tournament, reverse_tournament from utils.mixins import AdministratorMixin from utils.views import ModelFormSetView @@ -91,6 +91,8 @@ class EditDebateAdjudicatorsView(BaseEditDebateOrPanelAdjudicatorsView): page_title = gettext_lazy("Edit Allocation") prefetch_adjs = True # Fetched in full as get_serialised + view_permission = Permission.VIEW_DEBATEADJUDICATORS + def get_extra_info(self): info = super().get_extra_info() return info @@ -105,6 +107,8 @@ class EditPanelAdjudicatorsView(BaseEditDebateOrPanelAdjudicatorsView): template_name = "edit_panel_adjudicators.html" page_title = gettext_lazy("Edit Panels") + view_permission = Permission.VIEW_PREFORMEDPANELS + def get_extra_info(self): info = super().get_extra_info() info['backUrl'] = reverse_tournament('panel-adjudicators-index', @@ -146,12 +150,6 @@ class BaseAdjudicatorConflictsView(LogActionMixin, AdministratorMixin, Tournamen formset_factory_kwargs = {} - def get_view_permission(self): - return 'view.%s' % (self.formset_model.__name__.lower()) - - def get_edit_permission(self): - return 'edit.%s' % (self.formset_model.__name__.lower()) - def get_formset_factory_kwargs(self): can_edit = has_permission(self.request.user, self.get_edit_permission(), self.tournament) kwargs = super().get_formset_factory_kwargs() @@ -187,6 +185,9 @@ def formset_valid(self, formset): class AdjudicatorTeamConflictsView(BaseAdjudicatorConflictsView): + view_permission = Permission.VIEW_ADJ_TEAM_CONFLICTS + edit_permission = Permission.EDIT_ADJ_TEAM_CONFLICTS + action_log_type = ActionLogEntry.ACTION_TYPE_CONFLICTS_ADJ_TEAM_EDIT formset_model = AdjudicatorTeamConflict page_title = gettext_lazy("Adjudicator-Team Conflicts") @@ -231,6 +232,9 @@ def add_message(self, nsaved, ndeleted): class AdjudicatorAdjudicatorConflictsView(BaseAdjudicatorConflictsView): + view_permission = Permission.VIEW_ADJ_ADJ_CONFLICTS + edit_permission = Permission.EDIT_ADJ_ADJ_CONFLICTS + action_log_type = ActionLogEntry.ACTION_TYPE_CONFLICTS_ADJ_ADJ_EDIT formset_model = AdjudicatorAdjudicatorConflict page_title = gettext_lazy("Adjudicator-Adjudicator Conflicts") @@ -271,6 +275,9 @@ def add_message(self, nsaved, ndeleted): class AdjudicatorInstitutionConflictsView(BaseAdjudicatorConflictsView): + view_permission = Permission.VIEW_ADJ_INST_CONFLICTS + edit_permission = Permission.EDIT_ADJ_INST_CONFLICTS + action_log_type = ActionLogEntry.ACTION_TYPE_CONFLICTS_ADJ_INST_EDIT formset_model = AdjudicatorInstitutionConflict page_title = gettext_lazy("Adjudicator-Institution Conflicts") @@ -310,6 +317,9 @@ def add_message(self, nsaved, ndeleted): class TeamInstitutionConflictsView(BaseAdjudicatorConflictsView): + view_permission = Permission.VIEW_TEAM_INST_CONFLICTS + edit_permission = Permission.EDIT_TEAM_INST_CONFLICTS + action_log_type = ActionLogEntry.ACTION_TYPE_CONFLICTS_TEAM_INST_EDIT formset_model = TeamInstitutionConflict page_title = gettext_lazy("Team-Institution Conflicts") diff --git a/tabbycat/adjfeedback/views.py b/tabbycat/adjfeedback/views.py index e9617a21553..b2aa5249339 100644 --- a/tabbycat/adjfeedback/views.py +++ b/tabbycat/adjfeedback/views.py @@ -672,7 +672,7 @@ def modify_adjudicator(self, request, adjudicator): class SetAdjudicatorBreakingStatusView(AdministratorMixin, TournamentMixin, LogActionMixin, View): action_log_type = ActionLogEntry.ACTION_TYPE_ADJUDICATOR_BREAK_SET - edit_permission = Permission.EDIT_SETBREAKING + edit_permission = Permission.EDIT_ADJ_BREAK def post(self, request, *args, **kwargs): body = self.request.body.decode('utf-8') diff --git a/tabbycat/availability/views.py b/tabbycat/availability/views.py index 09a4a3e382e..86ec36440bb 100644 --- a/tabbycat/availability/views.py +++ b/tabbycat/availability/views.py @@ -21,6 +21,7 @@ from draw.models import Debate from participants.models import Adjudicator, Team from tournaments.mixins import RoundMixin +from users.permissions import Permission from utils.misc import reverse_round from utils.mixins import AdministratorMixin from utils.tables import TabbycatTableBuilder @@ -206,6 +207,10 @@ def get_table(self): class AvailabilityTypeTeamView(AvailabilityTypeBase): + + view_permission = Permission.VIEW_ROUNDAVAILABILITIES_TEAM + edit_permission = Permission.EDIT_ROUNDAVAILABILITIES_TEAM + page_title = gettext_lazy("Team Availability") page_emoji = '๐Ÿ‘‚' model = Team @@ -225,6 +230,10 @@ def annotate_checkins(queryset, t): class AvailabilityTypeAdjudicatorView(AvailabilityTypeBase): + + view_permission = Permission.VIEW_ROUNDAVAILABILITIES_ADJ + edit_permission = Permission.EDIT_ROUNDAVAILABILITIES_ADJ + page_title = gettext_lazy("Adjudicator Availability") page_emoji = '๐Ÿ‘‚' model = Adjudicator @@ -244,6 +253,10 @@ def annotate_checkins(queryset, t): class AvailabilityTypeVenueView(AvailabilityTypeBase): + + view_permission = Permission.VIEW_ROUNDAVAILABILITIES_VENUE + edit_permission = Permission.EDIT_ROUNDAVAILABILITIES_VENUE + page_title = gettext_lazy("Room Availability") page_emoji = '๐ŸŽช' model = Venue diff --git a/tabbycat/checkins/views.py b/tabbycat/checkins/views.py index 43ebd90fb63..a275674a0ca 100644 --- a/tabbycat/checkins/views.py +++ b/tabbycat/checkins/views.py @@ -15,6 +15,7 @@ from participants.models import Person, Speaker from participants.serializers import InstitutionSerializer from tournaments.mixins import PublicTournamentPageMixin, TournamentMixin +from users.permissions import Permission from utils.misc import reverse_tournament from utils.mixins import AdministratorMixin, AssistantMixin from utils.views import PostOnlyRedirectView @@ -63,6 +64,8 @@ class CheckInPeopleStatusView(BaseCheckInStatusView): page_title = _("People's Check-In Statuses") window_preference = 'checkin_window_people' + edit_permission = Permission.EDIT_PARTICIPANT_CHECKIN + def get_context_data(self, **kwargs): team_codes = use_team_code_names(self.tournament, admin=self.for_admin) @@ -120,6 +123,8 @@ class CheckInVenuesStatusView(BaseCheckInStatusView): page_title = _("Rooms' Check-In Statuses") window_preference = 'checkin_window_venues' + edit_permission = Permission.EDIT_ROOM_CHECKIN + def get_context_data(self, **kwargs): venues = [] for venue in self.tournament.relevant_venues.select_related('checkin_identifier').prefetch_related('venuecategory_set').all(): diff --git a/tabbycat/draw/views.py b/tabbycat/draw/views.py index 4859ae37d5e..b6e294cc629 100644 --- a/tabbycat/draw/views.py +++ b/tabbycat/draw/views.py @@ -34,6 +34,7 @@ TournamentMixin) from tournaments.models import Round from tournaments.utils import get_side_name +from users.permissions import Permission from utils.misc import reverse_round, reverse_tournament from utils.mixins import AdministratorMixin from utils.tables import TabbycatTableBuilder @@ -250,6 +251,8 @@ class BriefingRoomDrawTableMixin: """Mixin for views that get projected in the briefing room, to be accessed only by admins and assistants.""" + view_permission = Permission.VIEW_BRIEFING_DRAW + def get_context_data(self, **kwargs): kwargs['no_popovers'] = True return super().get_context_data(**kwargs) @@ -445,6 +448,8 @@ def get_queryset(self): class AdminDrawView(RoundMixin, AdministratorMixin, AdminDrawUtilitiesMixin, VueTableTemplateView): detailed = False + view_permission = Permission.VIEW_ADMIN_DRAW + def get_page_title(self): round = self.round self.page_emoji = '๐Ÿ‘€' @@ -655,7 +660,7 @@ class DrawStatusEdit(LogActionMixin, AdministratorMixin, RoundMixin, PostOnlyRed class CreateDrawView(DrawStatusEdit): - + edit_permission = Permission.GENERATE_DEBATE action_log_type = ActionLogEntry.ACTION_TYPE_DRAW_CREATE def post(self, request, *args, **kwargs): @@ -741,6 +746,7 @@ class ConfirmDrawRegenerationView(AdministratorMixin, TemplateView): class DrawReleaseView(DrawStatusEdit): + edit_permission = Permission.RELEASE_DRAW action_log_type = ActionLogEntry.ACTION_TYPE_DRAW_RELEASE round_redirect_pattern_name = 'draw-display' @@ -761,6 +767,7 @@ def post(self, request, *args, **kwargs): class DrawUnreleaseView(DrawStatusEdit): + edit_permission = Permission.UNRELEASE_DRAW action_log_type = ActionLogEntry.ACTION_TYPE_DRAW_UNRELEASE round_redirect_pattern_name = 'draw-display' @@ -777,6 +784,7 @@ def post(self, request, *args, **kwargs): class SetRoundStartTimeView(DrawStatusEdit): + edit_permission = Permission.EDIT_STARTTIME action_log_type = ActionLogEntry.ACTION_TYPE_ROUND_START_TIME_SET round_redirect_pattern_name = 'draw-display' @@ -828,6 +836,7 @@ def get_table(self): class SideAllocationsView(AdministratorMixin, BaseSideAllocationsView): + view_permission = Permission.EDIT_ALLOCATESIDES pass @@ -839,6 +848,7 @@ class EditDebateTeamsView(DebateDragAndDropMixin, AdministratorMixin, TemplateVi template_name = "edit_debate_teams.html" page_title = gettext_lazy("Edit Matchups") prefetch_teams = False # Fetched in full as get_serialised + edit_permission = Permission.EDIT_DEBATETEAMS def get_serialised_allocatable_items(self): # TODO: account for shared teams diff --git a/tabbycat/importer/views.py b/tabbycat/importer/views.py index 12aeacf59c1..05e7fe8ca5d 100644 --- a/tabbycat/importer/views.py +++ b/tabbycat/importer/views.py @@ -22,6 +22,7 @@ from participants.utils import populate_code_names from tournaments.mixins import TournamentMixin from tournaments.models import Tournament +from users.permissions import Permission from utils.misc import redirect_tournament, reverse_tournament from utils.mixins import AdministratorMixin from utils.views import PostOnlyRedirectView @@ -97,6 +98,7 @@ def done(self, form_list, form_dict, **kwargs): class ImportInstitutionsWizardView(BaseImportWizardView): model = Institution + edit_permission = Permission.ADD_INSTITUTIONS form_list = [ ('raw', ImportInstitutionsRawForm), ('details', modelformset_factory(Institution, fields=('name', 'code'), extra=0)), @@ -112,6 +114,7 @@ def get_message(self, count): class ImportVenuesWizardView(BaseImportWizardView): model = Venue + edit_permission = Permission.ADD_ROOMS form_list = [ ('raw', ImportVenuesRawForm), ('details', modelformset_factory(Venue, form=VenueDetailsForm, extra=0)), @@ -171,6 +174,7 @@ def get_details_instance_initial(self): class ImportTeamsWizardView(BaseImportByInstitutionWizardView): model = Team + edit_permission = Permission.ADD_TEAMS form_list = [ ('numbers', ImportTeamsNumbersForm), ('details', modelformset_factory(Team, form=TeamDetailsForm, formset=TeamDetailsFormSet, extra=0)), @@ -193,6 +197,7 @@ def get_message(self, count): class ImportAdjudicatorsWizardView(BaseImportByInstitutionWizardView): model = Adjudicator + edit_permission = Permission.ADD_ADJUDICATORS form_list = [ ('numbers', ImportAdjudicatorsNumbersForm), ('details', modelformset_factory(Adjudicator, form=AdjudicatorDetailsForm, extra=0)), diff --git a/tabbycat/motions/views.py b/tabbycat/motions/views.py index aaa6ddebf72..15432b9d996 100644 --- a/tabbycat/motions/views.py +++ b/tabbycat/motions/views.py @@ -14,6 +14,7 @@ from tournaments.mixins import (CurrentRoundMixin, OptionalAssistantTournamentPageMixin, PublicTournamentPageMixin, RoundMixin, TournamentMixin) from tournaments.models import Round +from users.permissions import Permission from utils.misc import redirect_round from utils.mixins import AdministratorMixin from utils.views import ModelFormSetView, PostOnlyRedirectView @@ -43,7 +44,8 @@ def get_context_data(self, **kwargs): class EditMotionsView(AdministratorMixin, LogActionMixin, RoundMixin, ModelFormSetView): # Django doesn't have a class-based view for formsets, so this implements # the form processing analogously to FormView, with less decomposition. - + view_permission = Permission.VIEW_MOTION + edit_permission = Permission.EDIT_MOTION template_name = 'motions_edit.html' action_log_type = ActionLogEntry.ACTION_TYPE_MOTION_EDIT formset_model = Motion @@ -167,7 +169,7 @@ def post(self, request, *args, **kwargs): class ReleaseMotionsView(BaseReleaseMotionsView): - + edit_permission = Permission.RELEASE_MOTION action_log_type = ActionLogEntry.ACTION_TYPE_MOTIONS_RELEASE motions_released = True @@ -199,6 +201,7 @@ def get_context_data(self, **kwargs): class AdminDisplayMotionsView(AdministratorMixin, BaseDisplayMotionsView): + view_permission = Permission.DISPLAY_MOTION pass @@ -266,6 +269,7 @@ class AdminRoundMotionStatisticsView(AdministratorMixin, RoundMotionStatisticsVi class AdminGlobalMotionStatisticsView(AdministratorMixin, GlobalMotionStatisticsView): + view_permission = Permission.VIEW_MOTIONSTAB pass diff --git a/tabbycat/participants/views.py b/tabbycat/participants/views.py index 83ed2561f85..c93e0ee9dc6 100644 --- a/tabbycat/participants/views.py +++ b/tabbycat/participants/views.py @@ -22,6 +22,7 @@ from tournaments.mixins import (PublicTournamentPageMixin, SingleObjectFromTournamentMixin, TournamentMixin) from tournaments.models import Round +from users.permissions import Permission from utils.misc import redirect_tournament, reverse_tournament from utils.mixins import AdministratorMixin, AssistantMixin from utils.tables import TabbycatTableBuilder @@ -69,6 +70,7 @@ def get_context_data(self, **kwargs): class AdminParticipantsListView(AdministratorMixin, BaseParticipantsListView): + view_permission = Permission.VIEW_PARTICIPANTS template_name = 'participants_list.html' admin = True @@ -118,6 +120,7 @@ def get_table(self): class AdminInstitutionsListView(AdministratorMixin, BaseInstitutionsListView): + view_permission = Permission.VIEW_INSTITUTIONS template_name = 'participants_list.html' admin = True @@ -323,7 +326,7 @@ class EditSpeakerCategoriesView(LogActionMixin, AdministratorMixin, TournamentMi # uniqueness checks will work. Since this is a superuser form, they can # access all tournaments anyway, so tournament forgery wouldn't be a # security risk. - + view_permission = Permission.VIEW_SPEAKER_CATEGORIES template_name = 'speaker_categories_edit.html' formset_model = SpeakerCategory action_log_type = ActionLogEntry.ACTION_TYPE_SPEAKER_CATEGORIES_EDIT @@ -386,6 +389,7 @@ class EditSpeakerCategoryEligibilityView(AdministratorMixin, TournamentMixin, Vu template_name = 'edit_speaker_eligibility.html' page_title = _("Speaker Category Eligibility") page_emoji = '๐Ÿฏ' + edit_permission = Permission.EDIT_SPEAKER_CATEGORIES def get_table(self): table = TabbycatTableBuilder(view=self, sort_key='team') diff --git a/tabbycat/privateurls/views.py b/tabbycat/privateurls/views.py index f49ebcc465d..725969d19e9 100644 --- a/tabbycat/privateurls/views.py +++ b/tabbycat/privateurls/views.py @@ -17,6 +17,7 @@ from participants.views import BaseRecordView from tournaments.mixins import PersonalizablePublicTournamentPageMixin, SingleObjectByRandomisedUrlMixin, TournamentMixin from tournaments.models import Round +from users.permissions import Permission from utils.misc import reverse_tournament from utils.mixins import AdministratorMixin from utils.tables import TabbycatTableBuilder @@ -60,6 +61,7 @@ def get_participants_to_email(self, already_sent: bool = False) -> 'QuerySet[Per class RandomisedUrlsView(RandomisedUrlsMixin, VueTableTemplateView): + view_permission = Permission.VIEW_PRIVATE_URLS template_name = 'private_urls.html' tables_orientation = 'columns' @@ -113,6 +115,7 @@ def get_tables(self) -> List[TabbycatTableBuilder]: class GenerateRandomisedUrlsView(AdministratorMixin, TournamentMixin, PostOnlyRedirectView): tournament_redirect_pattern_name = 'privateurls-list' + edit_permission = Permission.GENERATE_PRIVATE_URLS def post(self, request: 'HttpRequest', *args, **kwargs) -> 'HttpResponseRedirect': tournament = self.tournament @@ -149,7 +152,8 @@ def post(self, request: 'HttpRequest', *args, **kwargs) -> 'HttpResponseRedirect class EmailRandomisedUrlsView(RoleColumnMixin, TournamentTemplateEmailCreateView): page_subtitle = _("Private URLs") - + view_permission = Permission.VIEW_PRIVATE_URLS_EMAIL_LIST + edit_permission = Permission.SEND_PRIVATE_URLS event = BulkNotification.EventType.URL subject_template = 'url_email_subject' message_template = 'url_email_message' diff --git a/tabbycat/results/views.py b/tabbycat/results/views.py index fb679c70163..479742de3d1 100644 --- a/tabbycat/results/views.py +++ b/tabbycat/results/views.py @@ -31,6 +31,7 @@ RoundMixin, SingleObjectByRandomisedUrlMixin, SingleObjectFromTournamentMixin, TournamentMixin) from tournaments.models import Round +from users.permissions import Permission from utils.misc import get_ip_address, reverse_round, reverse_tournament from utils.mixins import AdministratorMixin, AssistantMixin from utils.tables import TabbycatTableBuilder @@ -378,6 +379,7 @@ def post(self, request, *args, **kwargs): class AdministratorBallotSetMixin(AdministratorMixin): template_name = 'ballot_entry.html' + edit_permission = Permission.ADD_BALLOTSUBMISSIONS tabroom = True def get_success_url(self): diff --git a/tabbycat/standings/views.py b/tabbycat/standings/views.py index b7cdd2dbc33..d40813bb4f4 100644 --- a/tabbycat/standings/views.py +++ b/tabbycat/standings/views.py @@ -18,6 +18,7 @@ from results.models import SpeakerScore, TeamScore from tournaments.mixins import PublicTournamentPageMixin, RoundMixin, SingleObjectFromTournamentMixin, TournamentMixin from tournaments.models import Round +from users.permissions import Permission from utils.misc import reverse_tournament from utils.mixins import AdministratorMixin from utils.tables import TabbycatTableBuilder @@ -483,6 +484,7 @@ def populate_result_missing(self, standings): class TeamStandingsView(AdministratorMixin, BaseTeamStandingsView): + view_permission = Permission.VIEW_TEAMSTANDINGS """Superuser team standings view.""" template_name = 'team_standings.html' # add info alerts rankings = ('rank',) @@ -621,6 +623,7 @@ def get_context_data(self, **kwargs): class DiversityStandingsView(AdministratorMixin, BaseDiversityStandingsView): for_public = False + view_permission = Permission.VIEW_DIVERSITYTAB class PublicDiversityStandingsView(PublicTournamentPageMixin, BaseDiversityStandingsView): diff --git a/tabbycat/users/groups.py b/tabbycat/users/groups.py index aeee7a0051a..e99d2deee7a 100644 --- a/tabbycat/users/groups.py +++ b/tabbycat/users/groups.py @@ -19,13 +19,55 @@ class BaseGroup: class Equity(BaseGroup): # Permissions to manage conflicts/constraints, view feedback + participant info name = _("Equity") - permissions = [] + permissions = [ + Permission.EDIT_ROOMCATEGORIES, + Permission.EDIT_ROOMCONSTRAINTS, + Permission.EDIT_ADJ_ADJ_CONFLICTS, + Permission.EDIT_ADJ_INST_CONFLICTS, + Permission.EDIT_ADJ_INST_CONFLICTS, + Permission.EDIT_TEAM_INST_CONFLICTS, + Permission.VIEW_PARTICIPANTS, + Permission.VIEW_TEAMS, + Permission.VIEW_ADJUDICATORS, + Permission.VIEW_ROOMS, + Permission.VIEW_INSTITUTIONS, + ] class AdjudicationCore(BaseGroup): # Permissions to make [preformed] allocations, view feedback, and create motions name = _("Adjudication Core") - permissions = [] + permissions = [ + Permission.EDIT_BASEJUDGESCORES_IND, + Permission.EDIT_DEBATEADJUDICATORS, + Permission.EDIT_FEEDBACK_CONFIRM, + Permission.EDIT_FEEDBACK_IGNORE, + Permission.EDIT_FEEDBACK_OVERVIEW, + Permission.EDIT_JUDGESCORES_BULK, + Permission.EDIT_MOTION, + Permission.EDIT_STARTTIME, + Permission.EDIT_PREFORMEDPANELS, + Permission.EDIT_RELEASEMOTION, + Permission.EDIT_UNRELEASEMOTION, + Permission.EDIT_ROOMALLOCATIONS, + Permission.EDIT_ALLOCATESIDES, + Permission.EDIT_ADJ_BREAK, + Permission.VIEW_BREAK, + Permission.VIEW_BREAK_OVERVIEW, + Permission.VIEW_MOTIONSTAB, + Permission.VIEW_DIVERSITYTAB, + Permission.VIEW_STANDINGS_OVERVIEW, + Permission.VIEW_TEAMSTANDINGS, + Permission.VIEW_SPEAKERSSTANDINGS, + Permission.VIEW_REPLIESSTANDINGS, + Permission.VIEW_FEEDBACK, + Permission.ADD_FEEDBACK, + Permission.VIEW_PARTICIPANTS, + Permission.VIEW_TEAMS, + Permission.VIEW_ADJUDICATORS, + Permission.VIEW_ROOMS, + Permission.VIEW_INSTITUTIONS, + ] class TabDirector(BaseGroup): @@ -37,4 +79,25 @@ class TabDirector(BaseGroup): class TabAssistant(BaseGroup): # Permissions to match the Assistant interface name = _("Tabulation Assistant") - permissions = [] + permissions = [ + Permission.ADD_BALLOTSUBMISSIONS, + Permission.MARK_OTHERS_BALLOTSUBMISSIONS, + Permission.VIEW_BALLOTSUBMISSION_GRAPH, + Permission.ADD_FEEDBACK, + Permission.VIEW_INSTITUTIONS, + Permission.VIEW_PARTICIPANTS, + Permission.EDIT_PARTICIPANT_CHECKIN, + Permission.EDIT_ROOM_CHECKIN, + Permission.VIEW_BRIEFING_DRAW, + Permission.DISPLAY_MOTION, + ] + + +class Language(BaseGroup): + name = _("Language") + permissions = [ + Permission.EDIT_BREAK_ELIGIBILITY, + Permission.EDIT_SPEAKER_CATEGORIES, + Permission.VIEW_PARTICIPANTS, + Permission.VIEW_TEAMS, + ] diff --git a/tabbycat/users/permissions.py b/tabbycat/users/permissions.py index 9dd7c832de5..014984046c6 100644 --- a/tabbycat/users/permissions.py +++ b/tabbycat/users/permissions.py @@ -25,6 +25,13 @@ class Permission(TextChoices): VIEW_TEAMS = 'view.team', _("view teams") ADD_TEAMS = 'add.team', _("add teams") + VIEW_ADJUDICATORS = 'view.adj', _("view adjudicators") + ADD_ADJUDICATORS = 'add.adj', _("add adjudicators") + VIEW_ROOMS = 'view.room', _("view rooms") + ADD_ROOMS = 'add.room', _("add rooms") + VIEW_INSTITUTIONS = 'view.inst', _("view institutions") + ADD_INSTITUTIONS = 'add.inst', _("add institutions") + VIEW_PARTICIPANTS = 'view.particpants', _("view participants") VIEW_ROUNDAVAILABILITIES_TEAM = 'view.roundavailability.team', _("view round availabilities for teams") VIEW_ROUNDAVAILABILITIES_ADJ = 'view.roundavailability.adjudicator', _("view round availabilities for adjudicators") @@ -33,6 +40,11 @@ class Permission(TextChoices): EDIT_ROUNDAVAILABILITIES_ADJ = 'edit.roundavailability.adjudicator', _("edit round availabilities for adjudicators") EDIT_ROUNDAVAILABILITIES_VENUE = 'edit.roundavailability.venue', _("edit round availabilities for rooms") + VIEW_ROOMCONSTRAINTS = 'view.roomconstraints', _("view room constraints") + VIEW_ROOMCATEGORIES = 'view.roomcategories', _("view room categories") + EDIT_ROOMCONSTRAINTS = 'edit.roomconstraints', _("edit room constraints") + EDIT_ROOMCATEGORIES = 'edit.roomcategories', _("edit room categories") + VIEW_DEBATES = 'view.debate', _("view debates (draw)") VIEW_ADMIN_DRAW = 'view.debate.admin', _("view debates (detailed draw)") GENERATE_DEBATE = 'generate.debate', _("generate debates (draw)") @@ -57,21 +69,20 @@ class Permission(TextChoices): VIEW_MOTION = 'view.roundmotion', _("view motion per round") EDIT_MOTION = 'edit.roundmotion', _("edit motion per round") - EDIT_RELEASEDRAW = 'edit.releasedraw', _("release draw to public") - EDIT_RELEASEMOTION = 'edit.releasemotion', _("release motion to public") + RELEASE_DRAW = 'release.draw', _("release draw to public") + RELEASE_MOTION = 'release.motion', _("release motion to public") + UNRELEASE_DRAW = 'unrelease.draw', _("unrelease draw to public") + UNRELEASE_MOTION = 'unrelease.motion', _("unrelease motion to public") EDIT_STARTTIME = 'edit.starttime', _("add debate start time") - # these permissions are already assigned to the assistants - VIEW_ALLCURRENTDRAWS_ROOM = 'view.allcurrentdrawsroom', _("view all current draws by room") - VIEW_ALLCURRENTDRAWS_TEAM = 'view.allcurrentdrawsteam', _("view all current draws by team") - VIEW_ROUNDDRAW_ROOM = 'view.rounddrawroom', _("view current round draw by room") - VIEW_ROUNDDRAW_TEAM = 'view.rounddrawteam', _("view current round draw by team") - VIEW_DISPLAYMOTION = 'view.displaymotion', _("view display room motion page") + VIEW_DRAW = 'view.draw', _("view draws") + VIEW_BRIEFING_DRAW = 'view.briefingdraw', _("view draws (for the briefing room)") + DISPLAY_MOTION = 'display.motion', _("display motion (for the briefing room)") VIEW_TOURNAMENTPREFERENCEMODEL = 'view.tournamentpreferencemodel', _("view tournament configuration") EDIT_TOURNAMENTPREFERENCEMODEL = 'edit.tournamentpreferencemodel', _("edit tournament configuration") VIEW_PREFORMEDPANELS = 'view.preformedpanels', _("view existing preformed panels") - EDIT_PREFORMEDPANELS = 'edit.preformedpanels', _("edit existing preformed panels") + EDIT_PREFORMEDPANELS = 'edit.preformedpanels', _("edit preformed panels") # standings tab VIEW_STANDINGS_OVERVIEW = 'view.standingsoverview', _("view the overviews of standings") @@ -82,18 +93,16 @@ class Permission(TextChoices): VIEW_DIVERSITYTAB = 'view.diversitytab', _("view the diversity tab") # Feedback tab - VIEW_FEEDBACK_OVERVIEW = 'view.feedbackoverview', _("view overview of judge feedback scores") - EDIT_FEEDBACK_OVERVIEW = 'edit.feedbackoverview', _("edit overview of judge feedback scores") - # not sure what the right most column of the overview page is called, but I'm calling it comments for now + VIEW_FEEDBACK_OVERVIEW = 'view.feedbackoverview', _("view overview of judge feedback") EDIT_JUDGESCORES_BULK = 'edit.judgescoresbulk', _("bulk update judge scores") EDIT_BASEJUDGESCORES_IND = 'edit.judgescoresind', _("edit base scores of judges") - EDIT_SETBREAKING = 'edit.setbreaking', _("edit breaking judges") VIEW_FEEDBACK = 'view.feedback', _("view feedback") EDIT_FEEDBACK_IGNORE = 'edit.feedbackignore', _("toggle ignore feedback") EDIT_FEEDBACK_CONFIRM = 'edit.feedbackconfirm', _("toggle confirm feedback") VIEW_FEEDBACK_UNSUBMITTED = 'view.feedbackunsubmitted', _("view feedback unsubmitted tab") ADD_FEEDBACK = 'add.feedback', _("add feedback") VIEW_ADJ_BREAK = 'view.adj.break', _("view adjudicator break") + EDIT_ADJ_BREAK = 'edit.adj.break', _("edit adjudicator break") # idk if its possible for them to add feedback everywhere, considering there is add feedback on multiple pages # breaks @@ -101,10 +110,21 @@ class Permission(TextChoices): VIEW_BREAK_ELIGIBILITY = 'view.breakeligibility', _("view break eligibility") EDIT_BREAK_CATEGORIES = 'edit.breakcategories', _("edit break categories") VIEW_BREAK_CATEGORIES = 'view.breakcategories', _("view break categories") + VIEW_SPEAKER_CATEGORIES = 'view.speakercategories', _("view speaker categories") + EDIT_SPEAKER_CATEGORIES = 'edit.speakercategories', _("edit speaker categories") VIEW_BREAK_OVERVIEW = 'view.break.overview', _("view break overview") VIEW_BREAK = 'view.break', _("view breaks") GENERATE_BREAK = 'generate.break', _("generate all breaks") + VIEW_PRIVATE_URLS = 'view.privateurls', _("view private urls") + VIEW_PRIVATE_URLS_EMAIL_LIST = 'view.privateurls.emaillist', _("view private urls email list") + GENERATE_PRIVATE_URLS = 'generate.privateurls', _("generate private URLs") + # need to get rid of generate private urls soons + SEND_PRIVATE_URLS = 'send.privateurls', _("send private URLs") + + EDIT_PARTICIPANT_CHECKIN = 'edit.participantcheckin', _("edit participant check-in") + EDIT_ROOM_CHECKIN = 'edit.roomcheckin', _("edit room check-in") + permission_type = Union[Permission, bool] diff --git a/tabbycat/venues/views.py b/tabbycat/venues/views.py index a64295e62f8..9ca4251114b 100644 --- a/tabbycat/venues/views.py +++ b/tabbycat/venues/views.py @@ -10,6 +10,7 @@ from actionlog.models import ActionLogEntry from availability.utils import annotate_availability from tournaments.mixins import DebateDragAndDropMixin, TournamentMixin +from users.permissions import Permission from utils.forms import SelectPrepopulated from utils.misc import ranks_dictionary, redirect_tournament, reverse_tournament from utils.mixins import AdministratorMixin @@ -23,6 +24,7 @@ class EditDebateVenuesView(DebateDragAndDropMixin, AdministratorMixin, TemplateView): + view_permission = Permission.VIEW_ROOMALLOCATIONS template_name = "edit_debate_venues.html" page_title = gettext_lazy("Edit Rooms") prefetch_venues = False # Fetched in full as get_serialised @@ -50,6 +52,7 @@ def get_extra_info(self): class VenueCategoriesView(LogActionMixin, AdministratorMixin, TournamentMixin, ModelFormSetView): + view_permission = Permission.VIEW_ROOMCATEGORIES template_name = 'venue_categories_edit.html' formset_model = VenueCategory action_log_type = ActionLogEntry.ACTION_TYPE_VENUE_CATEGORIES_EDIT @@ -97,6 +100,7 @@ def get_success_url(self, *args, **kwargs): class VenueConstraintsView(AdministratorMixin, LogActionMixin, TournamentMixin, ModelFormSetView): + view_permission = Permission.VIEW_ROOMCONSTRAINTS template_name = 'venue_constraints_edit.html' formset_model = VenueConstraint action_log_type = ActionLogEntry.ACTION_TYPE_VENUE_CONSTRAINTS_EDIT From b319f6bc12ebd370e4810b29ce5cb8e31b4dd3a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Sun, 28 Jan 2024 21:14:47 -0400 Subject: [PATCH 11/27] Add permissions to view deanonymized/decoded names This commit adds the `VIEW_DECODED_TEAMS` and `VIEW_ANONYMOUS` permissions in order to regulate whether users can see teams' real names or the names of people who have been anonymised. These permissions are then hooked up where needed. The permissions are added to Equity and Adj Core by default. --- tabbycat/actionlog/models.py | 10 ++++++---- tabbycat/adjfeedback/views.py | 3 +-- tabbycat/checkins/views.py | 2 +- tabbycat/draw/views.py | 1 - tabbycat/motions/views.py | 4 +--- tabbycat/options/utils.py | 7 +++++-- tabbycat/participants/templatetags/participant_link.py | 2 +- tabbycat/participants/views.py | 4 ++-- tabbycat/users/groups.py | 4 ++++ tabbycat/users/permissions.py | 2 ++ tabbycat/utils/tables.py | 9 +++++---- 11 files changed, 28 insertions(+), 20 deletions(-) diff --git a/tabbycat/actionlog/models.py b/tabbycat/actionlog/models.py index 7e8932340ff..f423062394e 100644 --- a/tabbycat/actionlog/models.py +++ b/tabbycat/actionlog/models.py @@ -190,7 +190,7 @@ def clean(self): if self.user is None and self.ip_address is None: raise ValidationError(_("All log entries require at least one of a user and an IP address.")) - def get_content_object_display(self, omit_tournament=False): + def get_content_object_display(self, omit_tournament=False, user=None): obj = self.content_object if obj is None: @@ -199,12 +199,12 @@ def get_content_object_display(self, omit_tournament=False): model_name = self.content_type.model try: if model_name == 'ballotsubmission': - if use_team_code_names(self.tournament, True): + if use_team_code_names(self.tournament, True, user): return obj.debate.matchup_codes else: return obj.debate.matchup elif model_name == 'debate': - if use_team_code_names(self.tournament, True): + if use_team_code_names(self.tournament, True, user): return obj.debate.matchup_codes else: return obj.debate.matchup @@ -229,6 +229,8 @@ def serialize(self): 'id': self.id, 'user': self.user.username if self.user else self.ip_address or _("anonymous"), 'type': self.get_type_display(), - 'param': self.get_content_object_display(omit_tournament=True), + # As the team names are passed in the content of the message for all users, + # must assume they don't have permission for real names + 'param': self.get_content_object_display(omit_tournament=True, user=None), 'timestamp': badge_datetime_format(self.timestamp), } diff --git a/tabbycat/adjfeedback/views.py b/tabbycat/adjfeedback/views.py index b2aa5249339..e8c8e31db8b 100644 --- a/tabbycat/adjfeedback/views.py +++ b/tabbycat/adjfeedback/views.py @@ -528,7 +528,6 @@ def get_success_url(self): class AdminAddFeedbackView(AdministratorMixin, BaseTabroomAddFeedbackView): edit_permission = Permission.ADD_FEEDBACK - pass class AssistantAddFeedbackView(AssistantMixin, BaseTabroomAddFeedbackView): @@ -601,7 +600,7 @@ class PublicAddFeedbackByIdUrlView(PublicAddFeedbackView): tabroom = False def get_team_short_name(self, team): - use_code_names = use_team_code_names(self.tournament, admin=False) + use_code_names = use_team_code_names(self.tournament, admin=False, user=self.request.user) return team.code_name if use_code_names else team.short_name def is_page_enabled(self, tournament): diff --git a/tabbycat/checkins/views.py b/tabbycat/checkins/views.py index a275674a0ca..4818cbf3d83 100644 --- a/tabbycat/checkins/views.py +++ b/tabbycat/checkins/views.py @@ -68,7 +68,7 @@ class CheckInPeopleStatusView(BaseCheckInStatusView): def get_context_data(self, **kwargs): - team_codes = use_team_code_names(self.tournament, admin=self.for_admin) + team_codes = use_team_code_names(self.tournament, admin=self.for_admin, user=self.request.user) kwargs["team_codes"] = json.dumps(team_codes) adjudicators = [] diff --git a/tabbycat/draw/views.py b/tabbycat/draw/views.py index b6e294cc629..e3fccc5cb03 100644 --- a/tabbycat/draw/views.py +++ b/tabbycat/draw/views.py @@ -837,7 +837,6 @@ def get_table(self): class SideAllocationsView(AdministratorMixin, BaseSideAllocationsView): view_permission = Permission.EDIT_ALLOCATESIDES - pass class PublicSideAllocationsView(PublicTournamentPageMixin, BaseSideAllocationsView): diff --git a/tabbycat/motions/views.py b/tabbycat/motions/views.py index 15432b9d996..14928d475db 100644 --- a/tabbycat/motions/views.py +++ b/tabbycat/motions/views.py @@ -202,7 +202,6 @@ def get_context_data(self, **kwargs): class AdminDisplayMotionsView(AdministratorMixin, BaseDisplayMotionsView): view_permission = Permission.DISPLAY_MOTION - pass class AssistantDisplayMotionsView(CurrentRoundMixin, OptionalAssistantTournamentPageMixin, BaseDisplayMotionsView): @@ -265,12 +264,11 @@ class BasePublicMotionStatisticsView(PublicTournamentPageMixin): class AdminRoundMotionStatisticsView(AdministratorMixin, RoundMotionStatisticsView): - pass + view_permission = Permission.VIEW_MOTIONSTAB class AdminGlobalMotionStatisticsView(AdministratorMixin, GlobalMotionStatisticsView): view_permission = Permission.VIEW_MOTIONSTAB - pass class PublicRoundMotionStatisticsView(BasePublicMotionStatisticsView, RoundMotionStatisticsView): diff --git a/tabbycat/options/utils.py b/tabbycat/options/utils.py index 9d4744678e6..cf65dd2e27b 100644 --- a/tabbycat/options/utils.py +++ b/tabbycat/options/utils.py @@ -1,20 +1,23 @@ import logging +from django.contrib.auth.models import AnonymousUser from django.forms import ValidationError from django.utils.encoding import force_str from django.utils.translation import gettext_lazy as _ +from users.permissions import has_permission, Permission + logger = logging.getLogger(__name__) -def use_team_code_names(tournament, admin): +def use_team_code_names(tournament, admin, user=AnonymousUser()): """Returns True if team code names should be used, given the tournament preferences of `tournament` and whether the request is for an admin view. `admin` should be True if the request is for an admin view and False if not. """ if tournament.pref('team_code_names') in ['admin-tooltips-real', 'everywhere']: return True - if tournament.pref('team_code_names') == 'admin-tooltips-code' and not admin: + if tournament.pref('team_code_names') == 'admin-tooltips-code' and not (admin and has_permission(user, Permission.VIEW_DECODED_TEAMS, tournament)): return True return False diff --git a/tabbycat/participants/templatetags/participant_link.py b/tabbycat/participants/templatetags/participant_link.py index f9d838fe232..29793ae1861 100644 --- a/tabbycat/participants/templatetags/participant_link.py +++ b/tabbycat/participants/templatetags/participant_link.py @@ -15,7 +15,7 @@ def team_record_link(context, team, admin, style=True): if not team or not context['tournament']: return "" - if use_team_code_names(context['tournament'], admin): + if use_team_code_names(context['tournament'], admin, user=context['user']): name = team.code_name else: name = team.short_name diff --git a/tabbycat/participants/views.py b/tabbycat/participants/views.py index c93e0ee9dc6..98d5ae255d1 100644 --- a/tabbycat/participants/views.py +++ b/tabbycat/participants/views.py @@ -51,7 +51,7 @@ def get_tables(self): speakers = Speaker.objects.filter(team__tournament=self.tournament).select_related( 'team', 'team__institution').prefetch_related('team__speaker_set', 'categories') - if use_team_code_names(self.tournament, self.admin): + if use_team_code_names(self.tournament, self.admin, user=self.request.user): speakers = speakers.order_by('team__code_name') else: speakers = speakers.order_by('team__short_name') @@ -195,7 +195,7 @@ def get_queryset(self): return super().get_queryset().select_related('institution__region') def use_team_code_names(self): - return use_team_code_names(self.tournament, self.admin) + return use_team_code_names(self.tournament, self.admin, user=self.request.user) @staticmethod def allocations_set(obj, admin, tournament): diff --git a/tabbycat/users/groups.py b/tabbycat/users/groups.py index e99d2deee7a..6f718973ea4 100644 --- a/tabbycat/users/groups.py +++ b/tabbycat/users/groups.py @@ -31,6 +31,8 @@ class Equity(BaseGroup): Permission.VIEW_ADJUDICATORS, Permission.VIEW_ROOMS, Permission.VIEW_INSTITUTIONS, + Permission.VIEW_DECODED_TEAMS, + Permission.VIEW_ANONYMOUS, ] @@ -67,6 +69,8 @@ class AdjudicationCore(BaseGroup): Permission.VIEW_ADJUDICATORS, Permission.VIEW_ROOMS, Permission.VIEW_INSTITUTIONS, + Permission.VIEW_DECODED_TEAMS, + Permission.VIEW_ANONYMOUS, ] diff --git a/tabbycat/users/permissions.py b/tabbycat/users/permissions.py index 014984046c6..5ec4d26a459 100644 --- a/tabbycat/users/permissions.py +++ b/tabbycat/users/permissions.py @@ -25,6 +25,8 @@ class Permission(TextChoices): VIEW_TEAMS = 'view.team', _("view teams") ADD_TEAMS = 'add.team', _("add teams") + VIEW_DECODED_TEAMS = 'view.teamname', _("view decoded team names") + VIEW_ANONYMOUS = 'view.anonymous', _("View names of anonymized participants") VIEW_ADJUDICATORS = 'view.adj', _("view adjudicators") ADD_ADJUDICATORS = 'add.adj', _("add adjudicators") VIEW_ROOMS = 'view.room', _("view rooms") diff --git a/tabbycat/utils/tables.py b/tabbycat/utils/tables.py index 8824da134ae..d5732ee3e83 100644 --- a/tabbycat/utils/tables.py +++ b/tabbycat/utils/tables.py @@ -18,6 +18,7 @@ from standings.templatetags.standingsformat import metricformat, rankingformat from tournaments.mixins import SingleObjectByRandomisedUrlMixin from tournaments.utils import get_side_name +from users.permissions import has_permission, Permission from utils.misc import reverse_round, reverse_tournament from .mixins import AdministratorMixin @@ -212,7 +213,7 @@ def _show_speakers_in_draw(self): @property def _use_team_code_names(self): - return use_team_code_names(self.tournament, self.admin) + return use_team_code_names(self.tournament, self.admin, user=self.view.request.user) def _team_short_name(self, team): """Returns the appropriate short name for the team, accounting for team code name preference.""" @@ -280,7 +281,7 @@ def _team_cell(self, team, show_emoji=False, subtext=None, highlight=False): cell['popover']['content'].append({'text': _("Real name: %(name)s") % {'name': escape(team.short_name)}}) if self._show_speakers_in_draw: - if self.admin: + if self.admin and has_permission(self.view.request.user, Permission.VIEW_ANONYMOUS, self.tournament): speakers = ["%s" % escape(s.name) if s.anonymous else escape(s.name) for s in team.speakers] else: speakers = [self.REDACTED_CELL['text'] if s.anonymous else escape(s.get_public_name(self.tournament)) for s in team.speakers] @@ -485,7 +486,7 @@ def add_adjudicator_columns(self, adjudicators, show_institutions=True, adj_data = [] for adj in adjudicators: - if adj.anonymous and not self.admin: + if adj.anonymous and not (self.admin and has_permission(self.view.request.user, Permission.VIEW_ANONYMOUS, self.tournament)): adj_data.append(self.REDACTED_CELL) else: cell = {'text': escape(adj.get_public_name(self.tournament))} @@ -657,7 +658,7 @@ def add_speaker_columns(self, speakers, categories=True): speaker_data = [] for speaker in speakers: anonymous = getattr(speaker, 'anonymise', False) or speaker.anonymous - if anonymous and not self.admin: + if anonymous and not (self.admin and has_permission(self.view.request.user, Permission.VIEW_ANONYMOUS, self.tournament)): speaker_data.append(self.REDACTED_CELL) else: cell = { From bf6350c1d68931d4f6b680dc9f4d1778117e3d66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Thu, 8 Feb 2024 08:53:42 -0400 Subject: [PATCH 12/27] API: Add groups endpoint & add permissions --- tabbycat/api/serializers.py | 18 +++++++++++------- tabbycat/api/urls.py | 13 +++++++++++-- tabbycat/api/views.py | 15 ++++++++++++++- tabbycat/users/permissions.py | 29 +++++++++++++++-------------- 4 files changed, 51 insertions(+), 24 deletions(-) diff --git a/tabbycat/api/serializers.py b/tabbycat/api/serializers.py index c0f6aa83944..8c0a2ce9bca 100644 --- a/tabbycat/api/serializers.py +++ b/tabbycat/api/serializers.py @@ -1404,14 +1404,10 @@ class Meta: class UserSerializer(serializers.ModelSerializer): - class TournamentPermissionsSerializer(serializers.Serializer): - class UserGroupSerializer(serializers.ModelSerializer): - class Meta: - model = Group - fields = ('name', 'permissions') - tournament = serializers.HyperlinkedIdentityField(view_name='api-tournament-detail') - groups = UserGroupSerializer(many=True, required=False) + class TournamentPermissionsSerializer(serializers.Serializer): + tournament = serializers.HyperlinkedIdentityField(view_name='api-tournament-detail', lookup_field='slug', lookup_url_kwarg='tournament_slug') + groups = fields.TournamentHyperlinkedRelatedField(many=True, view_name='api-group-detail', queryset=Group.objects.all(), default=[]) permissions = serializers.ListField(child=serializers.ChoiceField(choices=Permission.choices), required=False) url = serializers.HyperlinkedIdentityField(view_name='api-user-detail') @@ -1428,3 +1424,11 @@ def create(self, validated_data): user.save() return user + + +class GroupSerializer(serializers.ModelSerializer): + url = fields.TournamentHyperlinkedIdentityField(view_name='api-group-detail') + + class Meta: + model = Group + exclude = ('tournament',) diff --git a/tabbycat/api/urls.py b/tabbycat/api/urls.py index c651b4bb5cb..192e9107052 100644 --- a/tabbycat/api/urls.py +++ b/tabbycat/api/urls.py @@ -225,6 +225,15 @@ name='api-venuecategory-detail'), ])), + path('/user-groups', include([ + path('', + views.GroupViewSet.as_view(list_methods), + name='api-group-list'), + path('/', + views.GroupViewSet.as_view(detail_methods), + name='api-group-detail'), + ])), + path('/', include(pref_router.urls)), # Preferences ])), ])), @@ -239,10 +248,10 @@ path('/users', include([ path('', - views.UsersViewSet.as_view(list_methods), + views.UserViewSet.as_view(list_methods), name='api-user-list'), path('/', - views.UsersViewSet.as_view(detail_methods), + views.UserViewSet.as_view(detail_methods), name='api-user-detail'), ])), ])), diff --git a/tabbycat/api/views.py b/tabbycat/api/views.py index 1519541668f..0f00026c20c 100644 --- a/tabbycat/api/views.py +++ b/tabbycat/api/views.py @@ -1106,7 +1106,7 @@ def add_blank(self, request, *args, **kwargs): partial_update=extend_schema(summary="Patch user", parameters=[id_parameter]), destroy=extend_schema(summary="Deactivate user", parameters=[id_parameter]), ) -class UsersViewSet(AdministratorAPIMixin, ModelViewSet): +class UserViewSet(AdministratorAPIMixin, ModelViewSet): serializer_class = serializers.UserSerializer def get_queryset(self): @@ -1123,3 +1123,16 @@ def get_object(self): def perform_destroy(self, instance): instance.is_active = False instance.save() + + +@extend_schema(tags=['users'], parameters=[tournament_parameter]) +@extend_schema_view( + list=extend_schema(summary="List all permission groups in tournament"), + create=extend_schema(summary="Create group"), + retrieve=extend_schema(summary="Get group", parameters=[id_parameter]), + update=extend_schema(summary="Update group", parameters=[id_parameter]), + partial_update=extend_schema(summary="Patch group", parameters=[id_parameter]), + destroy=extend_schema(summary="Delete group", parameters=[id_parameter]), +) +class GroupViewSet(TournamentAPIMixin, AdministratorAPIMixin, ModelViewSet): + serializer_class = serializers.GroupSerializer diff --git a/tabbycat/users/permissions.py b/tabbycat/users/permissions.py index 5ec4d26a459..09c33f0adf4 100644 --- a/tabbycat/users/permissions.py +++ b/tabbycat/users/permissions.py @@ -1,5 +1,5 @@ from itertools import groupby -from typing import TYPE_CHECKING, Union +from typing import List, TYPE_CHECKING, Union from django.core.cache import cache from django.db.models import TextChoices @@ -165,16 +165,17 @@ def has_permission(user: 'settings.AUTH_USER_MODEL', permission: permission_type return perm -def get_permissions(user: 'settings.AUTH_USER_MODEL') -> dict: - groups = dict(groupby(user.membership_set.select_related('group', 'group__tournament').order_by('group__tournament').all(), key=lambda m: m.group.tournament)) - permissions = dict(groupby(user.userpermission_set.select_related('tournament').order_by('tournament').all(), key=lambda p: p.tournament)) - - user_perms = [] - for tournament in set(list(groups.keys()) + list(permissions.keys())): - user_perms.append({ - 'tournament': tournament, - 'groups': [m.group for m in groups.get(tournament, [])], - 'permissions': [p.permission for p in permissions.get(tournament, [])], - }) - - return user_perms +def get_permissions(user: 'settings.AUTH_USER_MODEL') -> List['Tournament']: + user_perms = {} + for t, groups in groupby(user.membership_set.select_related('group', 'group__tournament').order_by('group__tournament').all(), key=lambda m: m.group.tournament): + tournament = user_perms.setdefault(t.id, t) + tournament.groups = [m.group for m in groups] + permissions = set() + for g in tournament.groups: + permissions |= set(g.permissions) + setattr(tournament, 'permissions', getattr(tournament, 'permissions', set()) | permissions) + for t, perms in groupby(user.userpermission_set.select_related('tournament').order_by('tournament').all(), key=lambda p: p.tournament): + tournament = user_perms.setdefault(t.id, t) + setattr(tournament, 'permissions', getattr(tournament, 'permissions', set()) | {p.permission for p in perms}) + + return list(user_perms.values()) From b596d54985bfb43e7edfbeddfeaa4d75167e7e93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Thu, 8 Feb 2024 22:43:57 -0400 Subject: [PATCH 13/27] API: Don't expose password hash --- tabbycat/api/serializers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tabbycat/api/serializers.py b/tabbycat/api/serializers.py index 8c0a2ce9bca..23215dbf0dc 100644 --- a/tabbycat/api/serializers.py +++ b/tabbycat/api/serializers.py @@ -1417,6 +1417,9 @@ class Meta: model = get_user_model() fields = ('id', 'url', 'username', 'password', 'is_superuser', 'is_staff', 'email', 'is_active', 'date_joined', 'last_login', 'tournaments') read_only_fields = ('date_joined', 'last_login') + extra_kwargs = { + 'password': {'write_only': True}, + } def create(self, validated_data): user = self.Meta.model(**validated_data) From 5dbfc7efca00fe6f6f2140b5e28fea30713e8fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Thu, 8 Feb 2024 22:43:57 -0400 Subject: [PATCH 14/27] Expose user within TableBuilder --- tabbycat/options/tests/test_utils.py | 20 ++++++++++++++------ tabbycat/utils/tables.py | 15 +++++++++++---- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/tabbycat/options/tests/test_utils.py b/tabbycat/options/tests/test_utils.py index d6013454271..0f4286d458d 100644 --- a/tabbycat/options/tests/test_utils.py +++ b/tabbycat/options/tests/test_utils.py @@ -1,5 +1,7 @@ from itertools import product +from django.contrib.auth import get_user_model +from django.contrib.auth.models import AnonymousUser from django.forms import ValidationError from django.test import TestCase @@ -7,26 +9,32 @@ from options.utils import use_team_code_names, use_team_code_names_data_entry, validate_metric_duplicates from standings.teams import TeamStandingsGenerator from tournaments.models import Tournament +from users.permissions import Permission + +User = get_user_model() class UseTeamCodeNamesTests(TestCase): def setUp(self): - Tournament.objects.create(slug="unittest", name="Unit Testing") + t = Tournament.objects.create(slug="unittest", name="Unit Testing") + self.user = User.objects.create(username='admin1', password='admin', is_active=True) + self.user.userpermission_set.create(tournament=t, permission=Permission.VIEW_DECODED_TEAMS) def tearDown(self): Tournament.objects.filter(slug='unittest').delete() + self.user.delete() def test_use_codes_if_setting(self): tournament = Tournament.objects.get(slug='unittest') - for setting, admin in product([c[0] for c in TeamCodeNames.choices], (False, True)): - with self.subTest(setting=setting, admin=admin): + for setting, admin, user in product([c[0] for c in TeamCodeNames.choices], (False, True), (self.user, AnonymousUser())): + with self.subTest(setting=setting, admin=admin, user=user.is_anonymous): tournament._prefs['team_code_names'] = setting - if setting in ['admin-tooltips-real', 'everywhere'] or (setting == 'admin-tooltips-code' and not admin): - self.assertTrue(use_team_code_names(tournament, admin)) + if setting in ['admin-tooltips-real', 'everywhere'] or (setting == 'admin-tooltips-code' and not (admin and not user.is_anonymous)): + self.assertTrue(use_team_code_names(tournament, admin, user)) else: - self.assertFalse(use_team_code_names(tournament, admin)) + self.assertFalse(use_team_code_names(tournament, admin, user)) def test_use_codes_data_entry(self): tournament = Tournament.objects.get(slug='unittest') diff --git a/tabbycat/utils/tables.py b/tabbycat/utils/tables.py index d5732ee3e83..2dfd6e81710 100644 --- a/tabbycat/utils/tables.py +++ b/tabbycat/utils/tables.py @@ -1,6 +1,7 @@ import logging import warnings +from django.contrib.auth.models import AnonymousUser from django.contrib.humanize.templatetags.humanize import ordinal from django.db.models import Exists, OuterRef, Prefetch from django.template.loader import render_to_string @@ -152,6 +153,10 @@ def jsondict(self): } +class FakeRequest(object): + user = AnonymousUser() + + class TabbycatTableBuilder(BaseTableBuilder): """Extends TableBuilder to add convenience functions specific to Tabbycat.""" @@ -190,6 +195,8 @@ def __init__(self, view=None, **kwargs): else: self.admin = kwargs.get('admin', False) + self.user = kwargs.get('user', getattr(view, 'request', FakeRequest()).user) + if isinstance(view, SingleObjectByRandomisedUrlMixin): self.private_url = True self.private_url_key = view.kwargs.get('url_key') @@ -213,7 +220,7 @@ def _show_speakers_in_draw(self): @property def _use_team_code_names(self): - return use_team_code_names(self.tournament, self.admin, user=self.view.request.user) + return use_team_code_names(self.tournament, self.admin, user=self.user) def _team_short_name(self, team): """Returns the appropriate short name for the team, accounting for team code name preference.""" @@ -281,7 +288,7 @@ def _team_cell(self, team, show_emoji=False, subtext=None, highlight=False): cell['popover']['content'].append({'text': _("Real name: %(name)s") % {'name': escape(team.short_name)}}) if self._show_speakers_in_draw: - if self.admin and has_permission(self.view.request.user, Permission.VIEW_ANONYMOUS, self.tournament): + if self.admin and has_permission(self.user, Permission.VIEW_ANONYMOUS, self.tournament): speakers = ["%s" % escape(s.name) if s.anonymous else escape(s.name) for s in team.speakers] else: speakers = [self.REDACTED_CELL['text'] if s.anonymous else escape(s.get_public_name(self.tournament)) for s in team.speakers] @@ -486,7 +493,7 @@ def add_adjudicator_columns(self, adjudicators, show_institutions=True, adj_data = [] for adj in adjudicators: - if adj.anonymous and not (self.admin and has_permission(self.view.request.user, Permission.VIEW_ANONYMOUS, self.tournament)): + if adj.anonymous and not (self.admin and has_permission(self.user, Permission.VIEW_ANONYMOUS, self.tournament)): adj_data.append(self.REDACTED_CELL) else: cell = {'text': escape(adj.get_public_name(self.tournament))} @@ -658,7 +665,7 @@ def add_speaker_columns(self, speakers, categories=True): speaker_data = [] for speaker in speakers: anonymous = getattr(speaker, 'anonymise', False) or speaker.anonymous - if anonymous and not (self.admin and has_permission(self.view.request.user, Permission.VIEW_ANONYMOUS, self.tournament)): + if anonymous and not (self.admin and has_permission(self.user, Permission.VIEW_ANONYMOUS, self.tournament)): speaker_data.append(self.REDACTED_CELL) else: cell = { From 0c7c76f6b1ebd8a90f981bb01f05a5a5dec5f86f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Sat, 9 Mar 2024 21:49:01 -0400 Subject: [PATCH 15/27] API: Implement permissions checks --- tabbycat/api/mixins.py | 10 +-- tabbycat/api/permissions.py | 24 +++++++ tabbycat/api/serializers.py | 110 +++++++++++++++++++----------- tabbycat/api/utils.py | 2 +- tabbycat/api/views.py | 123 +++++++++++++++++++++++++++++++++- tabbycat/users/permissions.py | 17 ++++- 6 files changed, 238 insertions(+), 48 deletions(-) diff --git a/tabbycat/api/mixins.py b/tabbycat/api/mixins.py index 24e15a54e09..1c199c68118 100644 --- a/tabbycat/api/mixins.py +++ b/tabbycat/api/mixins.py @@ -5,7 +5,7 @@ from tournaments.models import Round, Tournament -from .permissions import APIEnabledPermission, IsAdminOrReadOnly, PublicIfReleasedPermission, PublicPreferencePermission +from .permissions import APIEnabledPermission, IsAdminOrReadOnly, PerTournamentPermissionRequired, PublicIfReleasedPermission, PublicPreferencePermission class TournamentAPIMixin: @@ -60,16 +60,16 @@ def get_serializer_context(self): class AdministratorAPIMixin: - permission_classes = [APIEnabledPermission, IsAdminUser] + permission_classes = [APIEnabledPermission, IsAdminUser | PerTournamentPermissionRequired] class TournamentPublicAPIMixin: - permission_classes = [APIEnabledPermission, PublicPreferencePermission] + permission_classes = [APIEnabledPermission, PublicPreferencePermission | PerTournamentPermissionRequired] class OnReleasePublicAPIMixin(TournamentPublicAPIMixin): - permission_classes = [APIEnabledPermission, PublicIfReleasedPermission] + permission_classes = [APIEnabledPermission, PublicIfReleasedPermission | PerTournamentPermissionRequired] class PublicAPIMixin: - permission_classes = [APIEnabledPermission, IsAdminOrReadOnly] + permission_classes = [APIEnabledPermission, IsAdminOrReadOnly | PerTournamentPermissionRequired] diff --git a/tabbycat/api/permissions.py b/tabbycat/api/permissions.py index 2021413391a..91af2e73361 100644 --- a/tabbycat/api/permissions.py +++ b/tabbycat/api/permissions.py @@ -1,6 +1,8 @@ from dynamic_preferences.registries import global_preferences_registry from rest_framework.permissions import BasePermission, SAFE_METHODS +from users.permissions import has_permission + class APIEnabledPermission(BasePermission): message = "The API has been disabled on this site." @@ -30,3 +32,25 @@ def has_object_permission(self, request, view, obj): class IsAdminOrReadOnly(BasePermission): def has_permission(self, request, view): return request.method in SAFE_METHODS or (request.user and request.user.is_staff) + + +class PerTournamentPermissionRequired(BasePermission): + def get_required_permissions(self, view): + """ + Given a model and an HTTP method, return the list of permission + codes that the user is required to have. + """ + return ({ + 'list': view.list_permission, + 'create': view.create_permission, + 'retrieve': view.list_permission, + 'update': view.update_permission, + 'partial_update': view.update_permission, + 'destroy': view.destroy_permission, + 'delete_all': view.destroy_permission, + 'add_blank': view.create_permission, + }).get(view.action, False) + + def has_permission(self, request, view): + perm = self.get_required_permission(view, request.method) + return has_permission(request.user, perm, view.tournament) diff --git a/tabbycat/api/serializers.py b/tabbycat/api/serializers.py index 23215dbf0dc..733eff425a9 100644 --- a/tabbycat/api/serializers.py +++ b/tabbycat/api/serializers.py @@ -1,6 +1,6 @@ from collections import OrderedDict from collections.abc import Mapping -from functools import partialmethod +from functools import partial, partialmethod from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError as DjangoValidationError @@ -28,7 +28,7 @@ from standings.teams import TeamStandingsGenerator from tournaments.models import Round, Tournament from users.models import Group -from users.permissions import Permission +from users.permissions import has_permission, Permission from venues.models import Venue, VenueCategory, VenueConstraint from . import fields @@ -188,10 +188,12 @@ class RoundLinksSerializer(serializers.Serializer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if not is_staff(kwargs.get('context')): - self.fields.pop('feedback_weight') + with_permission = partial(has_permission, user=kwargs['context']['request'].user, tournament=kwargs['context']['tournament']) + if not with_permission(Permission.VIEW_FEEDBACK_OVERVIEW): + self.fields.pop('feedback_weight') # Can't show in a ListSerializer - if isinstance(self.instance, QuerySet) or not self.instance.motions_released: + if not with_permission(Permission.VIEW_MOTION) and (isinstance(self.instance, QuerySet) or not self.instance.motions_released): self.fields.pop('motions') class Meta: @@ -210,6 +212,9 @@ def validate(self, data): def create(self, validated_data): motions_data = validated_data.pop('roundmotion_set', []) + if len(motions_data) > 0 and not has_permission(self.context['request'].user, Permission.EDIT_MOTION): + raise serializers.PermissionDenied('Editing motions disallowed') + round = super().create(validated_data) if len(motions_data) > 0: @@ -224,6 +229,8 @@ def create(self, validated_data): def update(self, instance, validated_data): motions_data = validated_data.pop('roundmotion_set', []) + if len(motions_data) > 0 and not has_permission(self.context['request'].user, Permission.EDIT_MOTION): + raise serializers.PermissionDenied('Editing motions disallowed') for i, roundmotion in enumerate(motions_data, start=1): roundmotion['seq'] = i @@ -467,13 +474,18 @@ class SpeakerLinksSerializer(serializers.Serializer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if not is_staff(kwargs.get('context')): - self.fields.pop('gender') - self.fields.pop('email') - self.fields.pop('phone') - self.fields.pop('pronoun') - self.fields.pop('url_key') - - if kwargs['context']['tournament'].pref('participant_code_names') == 'everywhere': + t = kwargs['context']['tournament'] + with_permission = partial(has_permission, user=kwargs['context']['request'].user, tournament=t) + if not with_permission(Permission.VIEW_PARTICIPANT_CONTACT): + self.fields.pop('email') + self.fields.pop('phone') + if not with_permission(Permission.VIEW_PARTICIPANT_GENDER): + self.fields.pop('gender') + self.fields.pop('pronoun') + if not with_permission(Permission.VIEW_PRIVATE_URLS): + self.fields.pop('url_key') + + if not with_permission(Permission.VIEW_PARTICIPANT_DECODED) and t.pref('participant_code_names') == 'everywhere': self.fields.pop('name') class Meta: @@ -532,26 +544,37 @@ def __init__(self, *args, **kwargs): # Remove private fields in the public endpoint if needed if not is_staff(kwargs.get('context')): - self.fields.pop('institution_conflicts') - self.fields.pop('team_conflicts') - self.fields.pop('adjudicator_conflicts') - self.fields.pop('venue_constraints') - t = kwargs['context']['tournament'] - if not t.pref('show_adjudicator_institutions'): + with_permission = partial(has_permission, user=kwargs['context']['request'].user, tournament=t) + + if not with_permission(Permission.VIEW_ADJ_INST_CONFLICTS): + self.fields.pop('institution_conflicts') + if not with_permission(Permission.VIEW_ADJ_TEAM_CONFLICTS): + self.fields.pop('team_conflicts') + if not with_permission(Permission.VIEW_ADJ_ADJ_CONFLICTS): + self.fields.pop('adjudicator_conflicts') + if not with_permission(Permission.VIEW_ROOMCONSTRAINTS): + self.fields.pop('venue_constraints') + + if not with_permission(Permission.VIEW_PARTICIPANT_INST) and not t.pref('show_adjudicator_institutions'): self.fields.pop('institution') - if not t.pref('public_breaking_adjs'): + if not with_permission(Permission.VIEW_ADJ_BREAK) and not t.pref('public_breaking_adjs'): self.fields.pop('breaking') - if t.pref('participant_code_names') == 'everywhere': + if not with_permission(Permission.VIEW_PARTICIPANT_DECODED) and not t.pref('participant_code_names') == 'everywhere': self.fields.pop('name') - self.fields.pop('base_score') - self.fields.pop('trainee') - self.fields.pop('gender') - self.fields.pop('email') - self.fields.pop('phone') - self.fields.pop('pronoun') - self.fields.pop('url_key') + if not with_permission(Permission.VIEW_FEEDBACK_OVERVIEW): + self.fields.pop('base_score') + self.fields.pop('trainee') + + if not with_permission(Permission.VIEW_PARTICIPANT_CONTACT): + self.fields.pop('email') + self.fields.pop('phone') + if not with_permission(Permission.VIEW_PARTICIPANT_GENDER): + self.fields.pop('gender') + self.fields.pop('pronoun') + if not with_permission(Permission.VIEW_PRIVATE_URLS): + self.fields.pop('url_key') class Meta: model = Adjudicator @@ -638,20 +661,25 @@ def __init__(self, *args, **kwargs): # Remove private fields in the public endpoint if needed if not is_staff(kwargs.get('context')): - self.fields.pop('institution_conflicts') - self.fields.pop('venue_constraints') - t = kwargs['context']['tournament'] - if t.pref('team_code_names') in ('admin-tooltips-code', 'admin-tooltips-real', 'everywhere'): + with_permission = partial(has_permission, user=kwargs['context']['request'].user, tournament=t) + + if not with_permission(Permission.VIEW_TEAM_INST_CONFLICTS): + self.fields.pop('institution_conflicts') + if not with_permission(Permission.VIEW_ROOMCONSTRAINTS): + self.fields.pop('venue_constraints') + + if not with_permission(Permission.VIEW_DECODED_TEAMS) and t.pref('team_code_names') in ('admin-tooltips-code', 'admin-tooltips-real', 'everywhere'): self.fields.pop('institution') self.fields.pop('use_institution_prefix') self.fields.pop('reference') self.fields.pop('short_reference') self.fields.pop('short_name') self.fields.pop('long_name') - elif not t.pref('show_team_institutions'): + elif not with_permission(Permission.VIEW_PARTICIPANT_INST) and not t.pref('show_team_institutions'): self.fields.pop('institution') self.fields.pop('use_institution_prefix') + if not t.pref('public_break_categories'): self.fields.pop('break_categories') @@ -749,7 +777,9 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if not is_staff(kwargs.get('context')): - self.fields.pop('venue_constraints') + with_permission = partial(has_permission, user=kwargs['context']['request'].user, tournament=kwargs['context']['tournament']) + if not with_permission(Permission.VIEW_ROOMCONSTRAINTS): + self.fields.pop('venue_constraints') def create(self, validated_data): venue_constraints = validated_data.pop('venue_constraints', []) @@ -791,8 +821,10 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if not is_staff(kwargs.get('context')): - self.fields.pop('teams') - self.fields.pop('adjudicators') + with_permission = partial(has_permission, user=kwargs['context']['request'].user, tournament=kwargs['context']['tournament']) + if not with_permission(Permission.VIEW_PARTICIPANT_INST): + self.fields.pop('teams') + self.fields.pop('adjudicators') class VenueSerializer(serializers.ModelSerializer): @@ -907,10 +939,12 @@ class Meta: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if not is_staff(kwargs.get('context')): - self.fields.pop('bracket') - self.fields.pop('room_rank') - self.fields.pop('importance') - self.fields.pop('result_status') + with_permission = partial(has_permission, user=kwargs['context']['request'].user, tournament=kwargs['context']['tournament']) + if not with_permission(Permission.VIEW_ADMIN_DRAW): + self.fields.pop('bracket') + self.fields.pop('room_rank') + self.fields.pop('importance') + self.fields.pop('result_status') class Meta: model = Debate diff --git a/tabbycat/api/utils.py b/tabbycat/api/utils.py index acbad4278f0..0fcf142869f 100644 --- a/tabbycat/api/utils.py +++ b/tabbycat/api/utils.py @@ -1,4 +1,4 @@ def is_staff(context): # OpenAPI generation does not have a view (sometimes context is also None in that circumstance). # Avoid redacting fields. - return context is None or 'view' not in context or context['request'].user.is_staff + return context is None or 'view' not in context or context['request'].user diff --git a/tabbycat/api/views.py b/tabbycat/api/views.py index 0f00026c20c..ec4bd2b43d9 100644 --- a/tabbycat/api/views.py +++ b/tabbycat/api/views.py @@ -36,13 +36,13 @@ from standings.teams import TeamStandingsGenerator from tournaments.mixins import TournamentFromUrlMixin from tournaments.models import Round, Tournament -from users.permissions import get_permissions +from users.permissions import get_permissions, Permission from venues.models import Venue, VenueCategory from . import serializers from .fields import ParticipantAvailabilityForeignKeyField from .mixins import AdministratorAPIMixin, PublicAPIMixin, RoundAPIMixin, TournamentAPIMixin, TournamentPublicAPIMixin -from .permissions import APIEnabledPermission, PublicPreferencePermission +from .permissions import APIEnabledPermission, PerTournamentPermissionRequired, PublicPreferencePermission tournament_parameter = OpenApiParameter('tournament_slug', description="The tournament's slug", type=str, location="path") @@ -131,6 +131,9 @@ class TournamentPreferenceViewSet(TournamentFromUrlMixin, AdministratorAPIMixin, queryset = TournamentPreferenceModel.objects.all() serializer_class = PreferenceSerializer + list_permission = Permission.VIEW_TOURNAMENTPREFERENCEMODEL + update_permission = Permission.EDIT_TOURNAMENTPREFERENCEMODEL + def get_related_instance(self): return self.tournament @@ -149,6 +152,10 @@ class RoundViewSet(TournamentAPIMixin, PublicAPIMixin, ModelViewSet): lookup_field = 'seq' lookup_url_kwarg = 'round_seq' + create_permission = Permission.CREATE_ROUND + update_permission = Permission.EDIT_ROUND + destroy_permission = False + def get_queryset(self): return super().get_queryset().select_related( 'break_category', 'break_category__tournament', @@ -169,6 +176,11 @@ class MotionViewSet(TournamentAPIMixin, TournamentPublicAPIMixin, ModelViewSet): access_preference = ('public_motions', 'motion_tab_released') access_operator = any + list_permission = Permission.VIEW_MOTION + create_permission = Permission.EDIT_MOTION + update_permission = Permission.EDIT_MOTION + destroy_permission = False + def get_queryset(self): filters = Q() if self.tournament.pref('public_motions') and not (self.tournament.pref('motion_tab_released') or self.request.user.is_staff): @@ -188,6 +200,11 @@ def get_queryset(self): class BreakCategoryViewSet(TournamentAPIMixin, PublicAPIMixin, ModelViewSet): serializer_class = serializers.BreakCategorySerializer + list_permission = Permission.VIEW_BREAK_CATEGORIES + create_permission = Permission.EDIT_BREAK_CATEGORIES + update_permission = Permission.EDIT_BREAK_CATEGORIES + destroy_permission = Permission.EDIT_BREAK_CATEGORIES + @extend_schema(tags=['speaker-categories'], parameters=[tournament_parameter]) @extend_schema_view( @@ -201,6 +218,11 @@ class BreakCategoryViewSet(TournamentAPIMixin, PublicAPIMixin, ModelViewSet): class SpeakerCategoryViewSet(TournamentAPIMixin, PublicAPIMixin, ModelViewSet): serializer_class = serializers.SpeakerCategorySerializer + list_permission = Permission.VIEW_SPEAKER_CATEGORIES + create_permission = Permission.EDIT_SPEAKER_CATEGORIES + update_permission = Permission.EDIT_SPEAKER_CATEGORIES + destroy_permission = Permission.EDIT_SPEAKER_CATEGORIES + def get_queryset(self): if not self.request.user or not self.request.user.is_staff: return super().get_queryset().filter(public=True) @@ -217,6 +239,10 @@ class BreakEligibilityView(TournamentAPIMixin, TournamentPublicAPIMixin, Retriev serializer_class = serializers.BreakEligibilitySerializer access_preference = 'public_break_categories' + list_permission = Permission.VIEW_BREAK_ELIGIBILITY + create_permission = Permission.EDIT_BREAK_ELIGIBILITY + update_permission = Permission.EDIT_BREAK_ELIGIBILITY + def get_queryset(self): return super().get_queryset().prefetch_related('team_set') @@ -231,6 +257,10 @@ class SpeakerEligibilityView(TournamentAPIMixin, TournamentPublicAPIMixin, Retri serializer_class = serializers.SpeakerEligibilitySerializer access_preference = 'public_participants' + list_permission = Permission.VIEW_SPEAKER_ELIGIBILITY + create_permission = Permission.EDIT_SPEAKER_ELIGIBILITY + update_permission = Permission.EDIT_SPEAKER_ELIGIBILITY + def get_queryset(self): qs = super().get_queryset().prefetch_related('speaker_set') if not self.request.user or not self.request.user.is_staff: @@ -246,6 +276,11 @@ class BreakingTeamsView(TournamentAPIMixin, TournamentPublicAPIMixin, GenerateBr pagination_class = None access_preference = 'public_breaking_teams' + list_permission = Permission.VIEW_BREAK + create_permission = Permission.GENERATE_BREAK + update_permission = Permission.GENERATE_BREAK + destroy_permission = Permission.GENERATE_BREAK + @property def break_category(self): if not hasattr(self, "_break_category"): @@ -293,6 +328,11 @@ class InstitutionViewSet(TournamentAPIMixin, TournamentPublicAPIMixin, ModelView serializer_class = serializers.PerTournamentInstitutionSerializer access_preference = 'public_institutions_list' + list_permission = Permission.VIEW_INSTITUTIONS + create_permission = Permission.ADD_INSTITUTIONS + update_permission = Permission.ADD_INSTITUTIONS + destroy_permission = Permission.ADD_INSTITUTIONS + def perform_create(self, serializer): serializer.save() @@ -324,6 +364,11 @@ class TeamViewSet(TournamentAPIMixin, TournamentPublicAPIMixin, ModelViewSet): serializer_class = serializers.TeamSerializer access_preference = 'public_participants' + list_permission = Permission.VIEW_TEAMS + create_permission = Permission.ADD_TEAMS + update_permission = Permission.ADD_TEAMS + destroy_permission = Permission.ADD_TEAMS + def get_queryset(self): category_prefetch = Prefetch('categories', queryset=SpeakerCategory.objects.all().select_related('tournament')) if not self.request.user or not self.request.user.is_staff: @@ -354,6 +399,11 @@ class AdjudicatorViewSet(TournamentAPIMixin, TournamentPublicAPIMixin, ModelView serializer_class = serializers.AdjudicatorSerializer access_preference = 'public_participants' + list_permission = Permission.VIEW_ADJUDICATORS + create_permission = Permission.ADD_ADJUDICATORS + update_permission = Permission.ADD_ADJUDICATORS + destroy_permission = Permission.ADD_ADJUDICATORS + def get_break_permission(self): return self.request.user.is_staff or self.tournament.pref('public_breaking_adjs') @@ -383,6 +433,11 @@ def get_queryset(self): class GlobalInstitutionViewSet(AdministratorAPIMixin, ModelViewSet): serializer_class = serializers.InstitutionSerializer + list_permission = Permission.VIEW_INSTITUTIONS + create_permission = Permission.ADD_INSTITUTIONS + update_permission = Permission.ADD_INSTITUTIONS + destroy_permission = Permission.ADD_INSTITUTIONS + def get_queryset(self): filters = Q() if self.request.query_params.get('region'): @@ -404,6 +459,11 @@ class SpeakerViewSet(TournamentAPIMixin, TournamentPublicAPIMixin, ModelViewSet) tournament_field = "team__tournament" access_preference = 'public_participants' + list_permission = Permission.VIEW_TEAMS + create_permission = Permission.ADD_TEAMS + update_permission = Permission.ADD_TEAMS + destroy_permission = Permission.ADD_TEAMS + def perform_create(self, serializer): serializer.save() @@ -427,6 +487,11 @@ def get_queryset(self): class VenueViewSet(TournamentAPIMixin, PublicAPIMixin, ModelViewSet): serializer_class = serializers.VenueSerializer + list_permission = Permission.VIEW_ROOMS + create_permission = Permission.ADD_ROOMS + update_permission = Permission.ADD_ROOMS + destroy_permission = Permission.ADD_ROOMS + def get_queryset(self): # Tournament must exist for URLs return super().get_queryset().select_related('tournament').prefetch_related( @@ -445,6 +510,11 @@ def get_queryset(self): class VenueCategoryViewSet(TournamentAPIMixin, PublicAPIMixin, ModelViewSet): serializer_class = serializers.VenueCategorySerializer + list_permission = Permission.VIEW_ROOMCATEGORIES + create_permission = Permission.EDIT_ROOMCATEGORIES + update_permission = Permission.EDIT_ROOMCATEGORIES + destroy_permission = Permission.EDIT_ROOMCATEGORIES + def get_queryset(self): # Tournament must exist for URLs return super().get_queryset().select_related('tournament').prefetch_related( @@ -458,6 +528,11 @@ class BaseCheckinsView(AdministratorAPIMixin, TournamentAPIMixin, APIView): lookup_field = 'pk' lookup_url_kwarg = None + list_permission = Permission.VIEW_CHECKIN + create_permission = Permission.EDIT_PARTICIPANT_CHECKIN + update_permission = Permission.EDIT_PARTICIPANT_CHECKIN + destroy_permission = Permission.EDIT_PARTICIPANT_CHECKIN + def get_object_queryset(self): lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]} @@ -594,6 +669,10 @@ class VenueCheckinsView(BaseCheckinsView): object_api_view = 'api-venue-detail' window_preference_pref = 'checkin_window_venues' + create_permission = Permission.EDIT_ROOM_CHECKIN + update_permission = Permission.EDIT_ROOM_CHECKIN + destroy_permission = Permission.EDIT_ROOM_CHECKIN + def get_metrics_params(generator): metrics = { @@ -663,6 +742,8 @@ class SubstantiveSpeakerStandingsView(BaseStandingsView): tournament_field = 'team__tournament' generator = SpeakerStandingsGenerator + list_permission = Permission.VIEW_SPEAKERSSTANDINGS + def get_queryset(self): category = self.request.query_params.get('category', None) if category is not None: @@ -692,6 +773,8 @@ class TeamStandingsView(BaseStandingsView): model = Team generator = TeamStandingsGenerator + list_permission = Permission.VIEW_TEAMSTANDINGS + def get_queryset(self): category = self.request.query_params.get('category', None) if category is not None: @@ -713,6 +796,8 @@ class SpeakerRoundStandingsRoundsView(TournamentAPIMixin, TournamentPublicAPIMix tournament_field = "team__tournament" access_preference = 'speaker_tab_released' + list_permission = Permission.VIEW_SPEAKERSSTANDINGS + def get_queryset(self): qs = super().get_queryset().prefetch_related(Prefetch('team__debateteam_set', queryset=DebateTeam.objects.all().select_related('debate__round__tournament'))) data = {s.id: s for s in qs.all()} @@ -752,6 +837,8 @@ class TeamRoundStandingsRoundsView(TournamentAPIMixin, TournamentPublicAPIMixin, serializer_class = serializers.TeamRoundScoresSerializer access_preference = 'team_tab_released' + list_permission = Permission.VIEW_TEAMSTANDINGS + def perform_create(self, serializer): serializer.save() @@ -807,7 +894,12 @@ def get_round_status(self, view): round_released_field = 'draw_status' round_released_value = Round.Status.RELEASED - permission_classes = [APIEnabledPermission, Permission] + """list_permission = Permission.VIEW_DEBATE + create_permission = Permission.GENERATE_DEBATE + update_permission = Permission.GENERATE_DEBATE + destroy_permission = Permission.GENERATE_DEBATE""" + + permission_classes = [APIEnabledPermission, Permission | PerTournamentPermissionRequired] def get_queryset(self): return super().get_queryset().select_related('round', 'round__tournament', 'venue', 'venue__tournament').prefetch_related( @@ -838,6 +930,11 @@ class BallotViewSet(RoundAPIMixin, TournamentPublicAPIMixin, ModelViewSet): tournament_field = 'debate__round__tournament' round_field = 'debate__round' + list_permission = Permission.VIEW_BALLOTSUBMISSIONS + create_permission = Permission.ADD_BALLOTSUBMISSIONS + update_permission = Permission.EDIT_BALLOTSUBMISSIONS + destroy_permission = Permission.MARK_BALLOTSUBMISSIONS + @property def debate(self): if hasattr(self, '_debate'): @@ -894,6 +991,11 @@ def destroy(self, request, *args, **kwargs): class FeedbackQuestionViewSet(TournamentAPIMixin, PublicAPIMixin, ModelViewSet): serializer_class = serializers.FeedbackQuestionSerializer + list_permission = True + create_permission = Permission.EDIT_FEEDBACKQUESTION + update_permission = Permission.EDIT_FEEDBACKQUESTION + destroy_permission = Permission.EDIT_FEEDBACKQUESTION + def get_queryset(self): filters = Q() if self.request.query_params.get('from_adj'): @@ -921,6 +1023,11 @@ class FeedbackViewSet(TournamentAPIMixin, AdministratorAPIMixin, ModelViewSet): serializer_class = serializers.FeedbackSerializer tournament_field = 'adjudicator__tournament' + list_permission = Permission.VIEW_FEEDBACK + create_permission = Permission.ADD_FEEDBACK + update_permission = Permission.EDIT_FEEDBACK_IGNORE + destroy_permission = Permission.EDIT_FEEDBACK_CONFIRM + def perform_create(self, serializer): serializer.save() @@ -963,6 +1070,11 @@ def get_queryset(self): class AvailabilitiesViewSet(RoundAPIMixin, AdministratorAPIMixin, APIView): serializer_class = serializers.AvailabilitiesSerializer # Isn't actually used + list_permission = Permission.VIEW_ROUNDAVAILABILITIES + create_permission = Permission.EDIT_ROUNDAVAILABILITIES + update_permission = Permission.EDIT_ROUNDAVAILABILITIES + destroy_permission = Permission.EDIT_ROUNDAVAILABILITIES + extra_params = [ OpenApiParameter('adjudicators', description='Only include adjudicators', required=False, type=bool, default=False), OpenApiParameter('teams', description='Only include teams', required=False, type=bool, default=False), @@ -1051,6 +1163,11 @@ class PreformedPanelViewSet(RoundAPIMixin, AdministratorAPIMixin, ModelViewSet): serializer_class = serializers.PreformedPanelSerializer lookup_url_kwarg = 'debate_pk' + list_permission = Permission.VIEW_PREFORMEDPANELS + create_permission = Permission.EDIT_PREFORMEDPANELS + update_permission = Permission.EDIT_PREFORMEDPANELS + destroy_permission = Permission.EDIT_PREFORMEDPANELS + @property def debate(self): if hasattr(self, '_debate'): diff --git a/tabbycat/users/permissions.py b/tabbycat/users/permissions.py index 09c33f0adf4..f479670a320 100644 --- a/tabbycat/users/permissions.py +++ b/tabbycat/users/permissions.py @@ -34,6 +34,10 @@ class Permission(TextChoices): VIEW_INSTITUTIONS = 'view.inst', _("view institutions") ADD_INSTITUTIONS = 'add.inst', _("add institutions") VIEW_PARTICIPANTS = 'view.particpants', _("view participants") + VIEW_PARTICIPANT_GENDER = 'view.participants.gender', _("view participants' gender information") + VIEW_PARTICIPANT_CONTACT = 'view.participants.contact', _("view participants' contact information") + VIEW_PARTICIPANT_DECODED = 'view.participants.decoded', _("view participants' real names") + VIEW_PARTICIPANT_INST = 'view.participants.inst', _("view participants' institution") VIEW_ROUNDAVAILABILITIES_TEAM = 'view.roundavailability.team', _("view round availabilities for teams") VIEW_ROUNDAVAILABILITIES_ADJ = 'view.roundavailability.adjudicator', _("view round availabilities for adjudicators") @@ -41,13 +45,15 @@ class Permission(TextChoices): EDIT_ROUNDAVAILABILITIES_TEAM = 'edit.roundavailability.team', _("edit round availabilities for teams") EDIT_ROUNDAVAILABILITIES_ADJ = 'edit.roundavailability.adjudicator', _("edit round availabilities for adjudicators") EDIT_ROUNDAVAILABILITIES_VENUE = 'edit.roundavailability.venue', _("edit round availabilities for rooms") + VIEW_ROUNDAVAILABILITIES = 'view.roundavailability', _("view round availabilities") + EDIT_ROUNDAVAILABILITIES = 'edit.roundavailability', _("edit round availabilities") VIEW_ROOMCONSTRAINTS = 'view.roomconstraints', _("view room constraints") VIEW_ROOMCATEGORIES = 'view.roomcategories', _("view room categories") EDIT_ROOMCONSTRAINTS = 'edit.roomconstraints', _("edit room constraints") EDIT_ROOMCATEGORIES = 'edit.roomcategories', _("edit room categories") - VIEW_DEBATES = 'view.debate', _("view debates (draw)") + VIEW_DEBATE = 'view.debate', _("view debates (draw)") VIEW_ADMIN_DRAW = 'view.debate.admin', _("view debates (detailed draw)") GENERATE_DEBATE = 'generate.debate', _("generate debates (draw)") EDIT_DEBATETEAMS = 'edit.debateteam', _("edit debate teams (pairings)") @@ -107,6 +113,8 @@ class Permission(TextChoices): EDIT_ADJ_BREAK = 'edit.adj.break', _("edit adjudicator break") # idk if its possible for them to add feedback everywhere, considering there is add feedback on multiple pages + EDIT_FEEDBACKQUESTION = 'edit.feedbackquestion', _("edit feedback questions") + # breaks EDIT_BREAK_ELIGIBILITY = 'edit.breakeligibility', _("edit break eligibility") VIEW_BREAK_ELIGIBILITY = 'view.breakeligibility', _("view break eligibility") @@ -114,6 +122,8 @@ class Permission(TextChoices): VIEW_BREAK_CATEGORIES = 'view.breakcategories', _("view break categories") VIEW_SPEAKER_CATEGORIES = 'view.speakercategories', _("view speaker categories") EDIT_SPEAKER_CATEGORIES = 'edit.speakercategories', _("edit speaker categories") + VIEW_SPEAKER_ELIGIBILITY = 'view.speakereligibility', _("view speaker eligibility") + EDIT_SPEAKER_ELIGIBILITY = 'edit.speakereligibility', _("edit speaker eligibility") VIEW_BREAK_OVERVIEW = 'view.break.overview', _("view break overview") VIEW_BREAK = 'view.break', _("view breaks") GENERATE_BREAK = 'generate.break', _("generate all breaks") @@ -124,9 +134,14 @@ class Permission(TextChoices): # need to get rid of generate private urls soons SEND_PRIVATE_URLS = 'send.privateurls', _("send private URLs") + VIEW_CHECKIN = 'view.checkin', _("view checkins") EDIT_PARTICIPANT_CHECKIN = 'edit.participantcheckin', _("edit participant check-in") EDIT_ROOM_CHECKIN = 'edit.roomcheckin', _("edit room check-in") + EDIT_ROUND = 'edit.round', _("edit round attributes") + DELETE_ROUND = 'delete.round', _("delete rounds") + CREATE_ROUND = 'add.round', _("create rounds") + permission_type = Union[Permission, bool] From 69ec040b78318b5ffda64bdf076b33bb2a5f1cf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Sat, 23 Mar 2024 00:07:17 -0400 Subject: [PATCH 16/27] Make sure no "False" perms are added to cache --- tabbycat/users/permissions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tabbycat/users/permissions.py b/tabbycat/users/permissions.py index f479670a320..9df153b5a6b 100644 --- a/tabbycat/users/permissions.py +++ b/tabbycat/users/permissions.py @@ -172,11 +172,11 @@ def has_permission(user: 'settings.AUTH_USER_MODEL', permission: permission_type perm = ( user.userpermission_set.filter(permission=permission, tournament=tournament).exists() or - user.membership_set.filter(group__permissions__contains=[permission]).exists() + user.membership_set.filter(group__permissions__contains=[permission], group__tournament=tournament).exists() ) - cache.set("user_%d_%s_%s_permission" % (user.pk, tournament.slug, str(permission)), perm) if perm: user._permissions[tournament.slug].add(permission) + cache.set("user_%d_%s_%s_permission" % (user.pk, tournament.slug, str(permission)), perm) return perm From b1a5456426f219d584b670e933be9e9c8afec100 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Sat, 23 Mar 2024 00:08:12 -0400 Subject: [PATCH 17/27] Use groups in invitation --- tabbycat/users/forms.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tabbycat/users/forms.py b/tabbycat/users/forms.py index 3151cd6bcf1..1d6809c8a81 100644 --- a/tabbycat/users/forms.py +++ b/tabbycat/users/forms.py @@ -21,23 +21,20 @@ def save(self, commit=True): class InviteUserForm(PasswordResetForm): - role = forms.ChoiceField(label=_("User role"), choices=( - ('assistant', _("Assistant")), - ('administrator', _("Administrator")), - )) - def __init__(self, tournament, *args, **kwargs): self.tournament = tournament super().__init__(*args, **kwargs) + self.fields['role'] = forms.ModelChoiceField(queryset=tournament.group_set.all()) + def get_users(self, email): user, created = get_user_model().objects.get_or_create( email=email, defaults={ - 'is_superuser': self.cleaned_data['role'] == 'administrator', 'username': email.split("@")[0], }, ) + user.membership_set.create(group=self.cleaned_data['role']) return [user] def save(self, *args, **kwargs): From ecd843b95046a22d40f8263e2de7497b722cca9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Mon, 25 Mar 2024 07:45:56 -0600 Subject: [PATCH 18/27] Update API permission class Also add permissions for user endpoints --- tabbycat/api/permissions.py | 4 ++-- tabbycat/api/views.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tabbycat/api/permissions.py b/tabbycat/api/permissions.py index 91af2e73361..4cbad113975 100644 --- a/tabbycat/api/permissions.py +++ b/tabbycat/api/permissions.py @@ -35,7 +35,7 @@ def has_permission(self, request, view): class PerTournamentPermissionRequired(BasePermission): - def get_required_permissions(self, view): + def get_required_permission(self, view): """ Given a model and an HTTP method, return the list of permission codes that the user is required to have. @@ -52,5 +52,5 @@ def get_required_permissions(self, view): }).get(view.action, False) def has_permission(self, request, view): - perm = self.get_required_permission(view, request.method) + perm = self.get_required_permission(view) return has_permission(request.user, perm, view.tournament) diff --git a/tabbycat/api/views.py b/tabbycat/api/views.py index ec4bd2b43d9..e2e92ea667c 100644 --- a/tabbycat/api/views.py +++ b/tabbycat/api/views.py @@ -14,6 +14,7 @@ from rest_framework.fields import DateTimeField from rest_framework.generics import GenericAPIView, get_object_or_404, RetrieveUpdateAPIView from rest_framework.mixins import ListModelMixin +from rest_framework.permissions import IsAdminUser from rest_framework.response import Response from rest_framework.reverse import reverse from rest_framework.views import APIView @@ -1225,6 +1226,7 @@ def add_blank(self, request, *args, **kwargs): ) class UserViewSet(AdministratorAPIMixin, ModelViewSet): serializer_class = serializers.UserSerializer + permission_classes = [IsAdminUser] def get_queryset(self): qs = get_user_model().objects.prefetch_related('membership_set__group__tournament', 'userpermission_set__tournament') From d5d95d2f869e0be26e07209523da9daeefb92619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Mon, 25 Mar 2024 20:21:37 -0600 Subject: [PATCH 19/27] Add permission check on remaining pages --- tabbycat/adjallocation/views.py | 3 ++- tabbycat/availability/views.py | 3 +++ tabbycat/checkins/views.py | 12 ++++++++---- tabbycat/draw/views.py | 13 +++++++------ tabbycat/importer/views.py | 2 ++ tabbycat/notifications/views.py | 3 +++ tabbycat/options/views.py | 6 ++++++ tabbycat/participants/views.py | 3 +++ tabbycat/printing/views.py | 6 ++++-- tabbycat/results/views.py | 4 ++++ tabbycat/standings/views.py | 8 ++++++-- tabbycat/users/permissions.py | 10 ++++++++++ tabbycat/utils/mixins.py | 3 +++ tabbycat/venues/views.py | 2 ++ 14 files changed, 63 insertions(+), 15 deletions(-) diff --git a/tabbycat/adjallocation/views.py b/tabbycat/adjallocation/views.py index a07240704c7..44a4fe11f70 100644 --- a/tabbycat/adjallocation/views.py +++ b/tabbycat/adjallocation/views.py @@ -128,9 +128,10 @@ def debates_or_panels_factory(self, panels): context={'round': self.round}) -class PanelAdjudicatorsIndexView(TemplateView, AdministratorMixin): +class PanelAdjudicatorsIndexView(AdministratorMixin, TournamentMixin, TemplateView): template_name = "preformed_index.html" page_title = gettext_lazy("Preformed Panels") + view_permission = True # ============================================================================== diff --git a/tabbycat/availability/views.py b/tabbycat/availability/views.py index 86ec36440bb..9d804f6ab57 100644 --- a/tabbycat/availability/views.py +++ b/tabbycat/availability/views.py @@ -37,6 +37,7 @@ class AvailabilityIndexView(RoundMixin, AdministratorMixin, TemplateView): template_name = 'availability_index.html' page_title = gettext_lazy("Availability") page_emoji = '๐Ÿ“' + view_permission = Permission.VIEW_ROUNDAVAILABILITIES def get_context_data(self, **kwargs): if self.round.prev: @@ -291,6 +292,7 @@ def annotate_checkins(queryset, t): class BaseBulkActivationView(RoundMixin, AdministratorMixin, PostOnlyRedirectView): round_redirect_pattern_name = 'availability-index' + edit_permission = Permission.EDIT_ROUNDAVAILABILITIES def post(self, request, *args, **kwargs): try: @@ -343,6 +345,7 @@ def activate_function(self): # ============================================================================== class BaseAvailabilityUpdateView(RoundMixin, AdministratorMixin, LogActionMixin, View): + edit_permission = Permission.EDIT_ROUNDAVAILABILITIES def post(self, request, *args, **kwargs): body = self.request.body.decode('utf-8') diff --git a/tabbycat/checkins/views.py b/tabbycat/checkins/views.py index 4818cbf3d83..a58da0fb876 100644 --- a/tabbycat/checkins/views.py +++ b/tabbycat/checkins/views.py @@ -38,6 +38,7 @@ def get_context_data(self, **kwargs): class AdminCheckInPreScanView(AdministratorMixin, CheckInPreScanView): scan_view = 'admin-checkin-scan' + edit_permission = Permission.EDIT_PARTICIPANT_CHECKIN class AssistantCheckInPreScanView(AssistantMixin, CheckInPreScanView): @@ -107,6 +108,8 @@ def get_context_data(self, **kwargs): class AdminCheckInPeopleStatusView(AdministratorMixin, CheckInPeopleStatusView): scan_view = 'admin-checkin-scan' + view_permission = Permission.VIEW_CHECKIN + edit_permission = Permission.EDIT_PARTICIPANT_CHECKIN class AssistantCheckInPeopleStatusView(AssistantMixin, CheckInPeopleStatusView): @@ -123,8 +126,6 @@ class CheckInVenuesStatusView(BaseCheckInStatusView): page_title = _("Rooms' Check-In Statuses") window_preference = 'checkin_window_venues' - edit_permission = Permission.EDIT_ROOM_CHECKIN - def get_context_data(self, **kwargs): venues = [] for venue in self.tournament.relevant_venues.select_related('checkin_identifier').prefetch_related('venuecategory_set').all(): @@ -143,6 +144,8 @@ def get_context_data(self, **kwargs): class AdminCheckInVenuesStatusView(AdministratorMixin, CheckInVenuesStatusView): scan_view = 'admin-checkin-scan' + view_permission = Permission.VIEW_CHECKIN + edit_permission = Permission.EDIT_ROOM_CHECKIN class AssistantCheckInVenuesStatusView(AssistantMixin, CheckInVenuesStatusView): @@ -197,7 +200,7 @@ def get_context_data(self, **kwargs): class AdminCheckInIdentifiersView(AdministratorMixin, CheckInIdentifiersView): - pass + view_permission = Permission.VIEW_CHECKIN class AssistantCheckInIdentifiersView(AssistantMixin, CheckInIdentifiersView): @@ -206,6 +209,7 @@ class AssistantCheckInIdentifiersView(AssistantMixin, CheckInIdentifiersView): class AdminCheckInGenerateView(AdministratorMixin, LogActionMixin, TournamentMixin, PostOnlyRedirectView): + edit_permission = Permission.VIEW_CHECKIN def get_action_log_type(self): if self.kwargs["kind"] == "speakers": @@ -252,7 +256,7 @@ def get_context_data(self, **kwargs): class AdminCheckInPrintablesView(AdministratorMixin, CheckInPrintablesView): - pass + view_permission = Permission.VIEW_CHECKIN class AssistantCheckInPrintablesView(AssistantMixin, CheckInPrintablesView): diff --git a/tabbycat/draw/views.py b/tabbycat/draw/views.py index e3fccc5cb03..0ec9acd5469 100644 --- a/tabbycat/draw/views.py +++ b/tabbycat/draw/views.py @@ -251,8 +251,6 @@ class BriefingRoomDrawTableMixin: """Mixin for views that get projected in the briefing room, to be accessed only by admins and assistants.""" - view_permission = Permission.VIEW_BRIEFING_DRAW - def get_context_data(self, **kwargs): kwargs['no_popovers'] = True return super().get_context_data(**kwargs) @@ -285,22 +283,22 @@ def populate_table(self, debates, table): class AdminDrawDisplayForSpecificRoundByVenueView(AdministratorMixin, BriefingRoomDrawByVenueTableMixin, BaseDisplayDrawForSpecificRoundTableView): - pass + view_permission = Permission.VIEW_BRIEFING_DRAW class AdminDrawDisplayForSpecificRoundByTeamView(AdministratorMixin, BriefingRoomDrawByTeamTableMixin, BaseDisplayDrawForSpecificRoundTableView): - pass + view_permission = Permission.VIEW_BRIEFING_DRAW class AdminDrawDisplayForCurrentRoundsByVenueView(AdministratorMixin, BriefingRoomDrawByVenueTableMixin, BaseDisplayDrawForCurrentRoundsTableView): - pass + view_permission = Permission.VIEW_BRIEFING_DRAW class AdminDrawDisplayForCurrentRoundsByTeamView(AdministratorMixin, BriefingRoomDrawByTeamTableMixin, BaseDisplayDrawForCurrentRoundsTableView): - pass + view_permission = Permission.VIEW_BRIEFING_DRAW class AssistantDrawDisplayForSpecificRoundByVenueView(OptionalAssistantTournamentPageMixin, @@ -379,6 +377,7 @@ class BaseDrawDisplayIndexView(AdminDrawUtilitiesMixin, RoundMixin, TemplateView class AdminDrawDisplayView(AdministratorMixin, BaseDrawDisplayIndexView): template_name = 'draw_display_admin.html' + view_permission = True class AssistantDrawDisplayView(CurrentRoundMixin, OptionalAssistantTournamentPageMixin, BaseDrawDisplayIndexView): @@ -657,6 +656,7 @@ def get_template_names(self): class DrawStatusEdit(LogActionMixin, AdministratorMixin, RoundMixin, PostOnlyRedirectView): round_redirect_pattern_name = 'draw' + view_permission = Permission.GENERATE_DEBATE class CreateDrawView(DrawStatusEdit): @@ -743,6 +743,7 @@ def post(self, request, *args, **kwargs): class ConfirmDrawRegenerationView(AdministratorMixin, TemplateView): template_name = "draw_confirm_regeneration.html" + view_permission = Permission.GENERATE_DEBATE class DrawReleaseView(DrawStatusEdit): diff --git a/tabbycat/importer/views.py b/tabbycat/importer/views.py index 05e7fe8ca5d..a28877228bd 100644 --- a/tabbycat/importer/views.py +++ b/tabbycat/importer/views.py @@ -277,9 +277,11 @@ def get_success_url(self): class ExportArchiveIndexView(AdministratorMixin, TournamentMixin, TemplateView): template_name = 'archive_export_index.html' + view_permission = Permission.EXPORT_XML class ExportArchiveAllView(AdministratorMixin, TournamentMixin, View): + view_permission = Permission.EXPORT_XML def get(self, request, *args, **kwargs): response = HttpResponse(self.get_xml(), content_type='text/xml; charset=utf-8') diff --git a/tabbycat/notifications/views.py b/tabbycat/notifications/views.py index ca5e2ff28f0..e6d121e2adb 100644 --- a/tabbycat/notifications/views.py +++ b/tabbycat/notifications/views.py @@ -19,6 +19,7 @@ from participants.models import Person from tournaments.mixins import RoundMixin, TournamentMixin +from users.permissions import Permission from utils.mixins import AdministratorMixin, WarnAboutLegacySendgridConfigVarsMixin from utils.tables import TabbycatTableBuilder from utils.views import VueTableTemplateView @@ -82,6 +83,7 @@ class EmailStatusView(AdministratorMixin, TournamentMixin, VueTableTemplateView) page_title = gettext_lazy("Email Statuses") page_emoji = '๐Ÿ“ค' template_name = 'email_statuses.html' + view_permission = Permission.VIEW_EMAIL_STATUSES tables_orientation = 'rows' @@ -210,6 +212,7 @@ class BaseSelectPeopleEmailView(AdministratorMixin, TournamentMixin, VueTableTem template_name = "email_participants.html" page_title = gettext_lazy("Email Participants") page_emoji = '๐Ÿ“ค' + edit_permission = Permission.SEND_EMAILS form_class = BasicEmailForm diff --git a/tabbycat/options/views.py b/tabbycat/options/views.py index 5eeb43b0c3c..8d6aa4f7b12 100644 --- a/tabbycat/options/views.py +++ b/tabbycat/options/views.py @@ -12,6 +12,7 @@ from actionlog.mixins import LogActionMixin from actionlog.models import ActionLogEntry from tournaments.mixins import TournamentMixin +from users.permissions import Permission from utils.misc import reverse_tournament from utils.mixins import AdministratorMixin @@ -24,6 +25,7 @@ class TournamentConfigIndexView(AdministratorMixin, TournamentMixin, TemplateView): template_name = "preferences_index.html" + view_permission = True def get_preset_options(self): """Returns a list of all preset classes.""" @@ -59,6 +61,8 @@ class TournamentPreferenceFormView(AdministratorMixin, LogActionMixin, Tournamen possible_registries = [global_preferences_registry, tournament_preferences_registry] section = None template_name = "preferences_section_set.html" + view_permission = Permission.VIEW_SETTINGS + edit_permission = Permission.EDIT_SETTINGS action_log_type = ActionLogEntry.ACTION_TYPE_OPTIONS_EDIT @@ -78,6 +82,8 @@ def get_form_class(self, *args, **kwargs): class SetPresetPreferencesView(AdministratorMixin, LogActionMixin, TournamentMixin, FormView): template_name = "preset_edit.html" page_emoji = 'โ”' + view_permission = Permission.VIEW_SETTINGS + edit_permission = Permission.EDIT_SETTINGS action_log_type = ActionLogEntry.ACTION_TYPE_OPTIONS_EDIT diff --git a/tabbycat/participants/views.py b/tabbycat/participants/views.py index 98d5ae255d1..d5521bad8dc 100644 --- a/tabbycat/participants/views.py +++ b/tabbycat/participants/views.py @@ -154,6 +154,7 @@ def get_table(self): class AdminCodeNamesListView(AdministratorMixin, BaseCodeNamesListView): template_name = 'participants_list.html' + view_permission = Permission.VIEW_DECODED_TEAMS class AssistantCodeNamesListView(AssistantMixin, BaseCodeNamesListView): @@ -287,6 +288,7 @@ def get_table(self): class TeamRecordView(AdministratorMixin, BaseTeamRecordView): admin = True + view_permission = Permission.VIEW_TEAMS def get_queryset(self): return super().get_queryset().prefetch_related( @@ -298,6 +300,7 @@ def get_queryset(self): class AdjudicatorRecordView(AdministratorMixin, BaseAdjudicatorRecordView): admin = True + view_permission = Permission.VIEW_ADJUDICATORS def get_queryset(self): return super().get_queryset().prefetch_related( diff --git a/tabbycat/printing/views.py b/tabbycat/printing/views.py index 7509d0af257..76a2fb5343d 100644 --- a/tabbycat/printing/views.py +++ b/tabbycat/printing/views.py @@ -17,6 +17,7 @@ from results.utils import side_and_position_names from tournaments.mixins import (CurrentRoundMixin, OptionalAssistantTournamentPageMixin, RoundMixin, TournamentMixin) +from users.permissions import Permission from utils.misc import reverse_tournament from utils.mixins import AdministratorMixin from venues.serializers import VenueSerializer @@ -120,7 +121,7 @@ def get_context_data(self, **kwargs): class AdminPrintFeedbackFormsView(AdministratorMixin, BasePrintFeedbackFormsView): - pass + view_permission = Permission.VIEW_DEBATE class AssistantPrintFeedbackFormsView(CurrentRoundMixin, OptionalAssistantTournamentPageMixin, BasePrintFeedbackFormsView): @@ -221,7 +222,7 @@ def get_context_data(self, **kwargs): class AdminPrintScoresheetsView(AdministratorMixin, BasePrintScoresheetsView): - pass + view_permission = Permission.VIEW_DEBATE class AssistantPrintScoresheetsView(CurrentRoundMixin, OptionalAssistantTournamentPageMixin, BasePrintScoresheetsView): @@ -229,6 +230,7 @@ class AssistantPrintScoresheetsView(CurrentRoundMixin, OptionalAssistantTourname class BasePrintableRandomisedURLs(TournamentMixin, AdministratorMixin, TemplateView): + view_permission = Permission.VIEW_PRIVATE_URLS template_name = 'randomised_url_sheets.html' diff --git a/tabbycat/results/views.py b/tabbycat/results/views.py index 479742de3d1..dc2ae648320 100644 --- a/tabbycat/results/views.py +++ b/tabbycat/results/views.py @@ -114,6 +114,7 @@ class AssistantResultsEntryView(AssistantMixin, CurrentRoundMixin, BaseResultsEn class AdminResultsEntryForRoundView(AdministratorMixin, BaseResultsEntryForRoundView): template_name = 'admin_results.html' + view_permission = Permission.VIEW_RESULTS def get_context_data(self, **kwargs): # Stopgap to warn user about potential database inconsistency, when @@ -379,6 +380,7 @@ def post(self, request, *args, **kwargs): class AdministratorBallotSetMixin(AdministratorMixin): template_name = 'ballot_entry.html' + view_permission = Permission.VIEW_BALLOTSUBMISSIONS edit_permission = Permission.ADD_BALLOTSUBMISSIONS tabroom = True @@ -388,6 +390,8 @@ def get_success_url(self): class OldAdministratorBallotSetMixin(AdministratorMixin): template_name = 'enter_results.html' + view_permission = Permission.VIEW_BALLOTSUBMISSIONS + edit_permission = Permission.ADD_BALLOTSUBMISSIONS tabroom = True def get_success_url(self): diff --git a/tabbycat/standings/views.py b/tabbycat/standings/views.py index d40813bb4f4..01033b34aa4 100644 --- a/tabbycat/standings/views.py +++ b/tabbycat/standings/views.py @@ -37,6 +37,7 @@ class StandingsIndexView(AdministratorMixin, RoundMixin, TemplateView): template_name = 'standings_index.html' + view_permission = Permission.VIEW_STANDINGS_OVERVIEW def get_context_data(self, **kwargs): speaks = SpeakerScore.objects.filter( @@ -322,6 +323,7 @@ def add_round_results(self, standings, rounds): class SpeakerStandingsView(AdministratorMixin, BaseSubstantiveSpeakerStandingsView): template_name = 'speaker_standings.html' # add info alerts + view_permission = Permission.VIEW_SPEAKERSSTANDINGS class PublicSpeakerTabView(PublicTabMixin, BaseSubstantiveSpeakerStandingsView): @@ -348,7 +350,7 @@ def get(self, request, *args, **kwargs): class SpeakerCategoryStandingsView(AdministratorMixin, BaseSpeakerCategoryStandingsView): - pass + view_permission = Permission.VIEW_SPEAKERSSTANDINGS class PublicSpeakerCategoryTabView(PublicTabMixin, BaseSpeakerCategoryStandingsView): @@ -404,6 +406,7 @@ def populate_result_missing(self, standings): class ReplyStandingsView(AdministratorMixin, BaseReplyStandingsView): template_name = 'reply_standings.html' # add an info alert + view_permission = Permission.VIEW_REPLIESSTANDINGS class PublicReplyTabView(PublicTabMixin, BaseReplyStandingsView): @@ -484,10 +487,10 @@ def populate_result_missing(self, standings): class TeamStandingsView(AdministratorMixin, BaseTeamStandingsView): - view_permission = Permission.VIEW_TEAMSTANDINGS """Superuser team standings view.""" template_name = 'team_standings.html' # add info alerts rankings = ('rank',) + view_permission = Permission.VIEW_TEAMSTANDINGS def show_ballots(self): return True @@ -528,6 +531,7 @@ def get(self, request, *args, **kwargs): class BreakCategoryStandingsView(AdministratorMixin, BaseBreakCategoryStandingsView): """Superuser team standings view for a break category.""" rankings = ('rank',) + view_permission = Permission.VIEW_TEAMSTANDINGS def show_ballots(self): return True diff --git a/tabbycat/users/permissions.py b/tabbycat/users/permissions.py index 9df153b5a6b..b4e5a439b97 100644 --- a/tabbycat/users/permissions.py +++ b/tabbycat/users/permissions.py @@ -74,6 +74,7 @@ class Permission(TextChoices): MARK_BALLOTSUBMISSIONS = 'mark.ballotsubmission', _("confirm/discard any ballot") MARK_OTHERS_BALLOTSUBMISSIONS = 'mark.ballotsubmission.others', _("confirm/discard others' ballots") VIEW_BALLOTSUBMISSION_GRAPH = 'view.ballotsubmission.graph', _("view ballot graph") + VIEW_RESULTS = 'view.results', _("view results entry page") VIEW_MOTION = 'view.roundmotion', _("view motion per round") EDIT_MOTION = 'edit.roundmotion', _("edit motion per round") @@ -83,6 +84,7 @@ class Permission(TextChoices): UNRELEASE_MOTION = 'unrelease.motion', _("unrelease motion to public") EDIT_STARTTIME = 'edit.starttime', _("add debate start time") VIEW_DRAW = 'view.draw', _("view draws") + VIEW_BRIEFING_DRAW = 'view.briefingdraw', _("view draws (for the briefing room)") DISPLAY_MOTION = 'display.motion', _("display motion (for the briefing room)") @@ -142,6 +144,14 @@ class Permission(TextChoices): DELETE_ROUND = 'delete.round', _("delete rounds") CREATE_ROUND = 'add.round', _("create rounds") + VIEW_EMAIL_STATUSES = 'view.emails', _("view email statuses") + SEND_EMAILS = 'send.emails', _("send participants email messages") + + EXPORT_XML = 'export.xml', _("export DebateXML") + + VIEW_SETTINGS = 'view.settings', _("view settings") + EDIT_SETTINGS = 'edit.settings', _("edit settings") + permission_type = Union[Permission, bool] diff --git a/tabbycat/utils/mixins.py b/tabbycat/utils/mixins.py index ed4b9585e91..092aa8f7d58 100644 --- a/tabbycat/utils/mixins.py +++ b/tabbycat/utils/mixins.py @@ -61,6 +61,7 @@ class AdministratorMixin(UserPassesTestMixin, ContextMixin): def get_context_data(self, **kwargs): kwargs["user_role"] = self.view_role + kwargs['can_edit'] = has_permission(self.request.user, self.get_edit_permission(), self.tournament) if hasattr(self, 'tournament') else None return super().get_context_data(**kwargs) def get_view_permission(self) -> Optional['permission_type']: @@ -70,6 +71,8 @@ def get_edit_permission(self) -> Optional['permission_type']: return self.edit_permission def test_func(self) -> bool: + if not hasattr(self, 'tournament'): + return self.request.user.is_superuser if self.request.method == 'GET' and self.get_view_permission() is not None: return has_permission(self.request.user, self.get_view_permission(), self.tournament) if self.request.method in ['POST', 'PUT'] and self.get_edit_permission() is not None: diff --git a/tabbycat/venues/views.py b/tabbycat/venues/views.py index 9ca4251114b..d2ecc2e2acd 100644 --- a/tabbycat/venues/views.py +++ b/tabbycat/venues/views.py @@ -53,6 +53,7 @@ def get_extra_info(self): class VenueCategoriesView(LogActionMixin, AdministratorMixin, TournamentMixin, ModelFormSetView): view_permission = Permission.VIEW_ROOMCATEGORIES + edit_permission = Permission.EDIT_ROOMCATEGORIES template_name = 'venue_categories_edit.html' formset_model = VenueCategory action_log_type = ActionLogEntry.ACTION_TYPE_VENUE_CATEGORIES_EDIT @@ -101,6 +102,7 @@ def get_success_url(self, *args, **kwargs): class VenueConstraintsView(AdministratorMixin, LogActionMixin, TournamentMixin, ModelFormSetView): view_permission = Permission.VIEW_ROOMCONSTRAINTS + edit_permission = Permission.EDIT_ROOMCONSTRAINTS template_name = 'venue_constraints_edit.html' formset_model = VenueConstraint action_log_type = ActionLogEntry.ACTION_TYPE_VENUE_CONSTRAINTS_EDIT From 398344e31da82905eb7a9379411aa4dff2d3f3ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Thu, 2 May 2024 10:02:25 -0300 Subject: [PATCH 20/27] Create groups with tournament --- tabbycat/tournaments/forms.py | 8 ++++++++ tabbycat/users/groups.py | 5 +++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/tabbycat/tournaments/forms.py b/tabbycat/tournaments/forms.py index d8798e85aa1..223f50b27b5 100644 --- a/tabbycat/tournaments/forms.py +++ b/tabbycat/tournaments/forms.py @@ -12,6 +12,8 @@ from breakqual.utils import auto_make_break_rounds from options.preferences import TournamentStaff from options.presets import all_presets, data_entry_presets_for_form, presets_for_form, PrivateURLs, public_presets_for_form, PublicForms, PublicInformation +from users.groups import all_groups +from users.models import Group from .models import Round, Tournament from .signals import update_tournament_cache @@ -49,6 +51,11 @@ def add_default_feedback_questions(tournament): answer_type=AdjudicatorFeedbackQuestion.ANSWER_TYPE_LONGTEXT) comments.save() + @staticmethod + def add_default_permission_groups(tournament: Tournament): + for group in all_groups(): + Group.objects.create(name=group.name, permissions=group.permissions, tournament=tournament) + def save(self): tournament = super(TournamentStartForm, self).save() auto_make_rounds(tournament, self.cleaned_data["num_prelim_rounds"]) @@ -68,6 +75,7 @@ def save(self): open_break.full_clean() open_break.save() + self.add_default_permission_groups(tournament) self.add_default_feedback_questions(tournament) tournament.current_round = tournament.round_set.order_by('seq').first() tournament.save() diff --git a/tabbycat/users/groups.py b/tabbycat/users/groups.py index 6f718973ea4..16b6bdf98a5 100644 --- a/tabbycat/users/groups.py +++ b/tabbycat/users/groups.py @@ -17,7 +17,7 @@ class BaseGroup: class Equity(BaseGroup): - # Permissions to manage conflicts/constraints, view feedback + participant info + # Permissions to manage conflicts/constraints, view participant info name = _("Equity") permissions = [ Permission.EDIT_ROOMCATEGORIES, @@ -33,6 +33,7 @@ class Equity(BaseGroup): Permission.VIEW_INSTITUTIONS, Permission.VIEW_DECODED_TEAMS, Permission.VIEW_ANONYMOUS, + Permission.VIEW_ADMIN_DRAW, ] @@ -44,8 +45,8 @@ class AdjudicationCore(BaseGroup): Permission.EDIT_DEBATEADJUDICATORS, Permission.EDIT_FEEDBACK_CONFIRM, Permission.EDIT_FEEDBACK_IGNORE, - Permission.EDIT_FEEDBACK_OVERVIEW, Permission.EDIT_JUDGESCORES_BULK, + Permission.EDIT_BASEJUDGESCORES_IND, Permission.EDIT_MOTION, Permission.EDIT_STARTTIME, Permission.EDIT_PREFORMEDPANELS, From b234434e814582bedefa2c92b6c221844a601a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Sat, 4 May 2024 17:35:58 -0300 Subject: [PATCH 21/27] Update to Django 5 This commit updates the Python version to 3.11 and bumps packages to their update, most notably Django to version 5. There were some problems with updating to 3.12 so we'll keep it lower for now. This new version of Django brings some deprecations, such as model table indexing being more explicit. Migrations are also needed for this. --- .python-version | 2 +- .readthedocs.yaml | 2 +- Dockerfile | 2 +- Pipfile | 2 +- Pipfile.lock | 2035 ++++++++++------- render.yaml | 2 +- runtime.txt | 2 +- ...icatorconflict_unique_together_and_more.py | 56 + tabbycat/adjallocation/models.py | 12 +- ...icatorfeedback_unique_together_and_more.py | 80 + tabbycat/adjfeedback/models.py | 12 +- tabbycat/api/serializers.py | 2 +- ...ndavailability_unique_together_and_more.py | 24 + tabbycat/availability/models.py | 6 +- ..._breakcategory_unique_together_and_more.py | 45 + tabbycat/breakqual/models.py | 10 +- ...sideallocation_unique_together_and_more.py | 24 + tabbycat/draw/models.py | 3 +- ...tionpreference_unique_together_and_more.py | 33 + tabbycat/motions/models.py | 6 +- .../0013_alter_bulknotification_event.py | 18 + tabbycat/notifications/views.py | 2 +- ...participant_tournam_160efa_idx_and_more.py | 60 + ...er_institution_unique_together_and_more.py | 22 + tabbycat/participants/models.py | 19 +- ..._results_spe_ballot__667598_idx_and_mor.py | 70 + tabbycat/results/models.py | 22 +- tabbycat/settings/core.py | 10 +- tabbycat/standings/teams.py | 2 +- ...11_alter_round_unique_together_and_more.py | 27 + tabbycat/tournaments/models.py | 4 +- tabbycat/utils/models.py | 8 + ...ons_alter_venue_index_together_and_more.py | 26 + tabbycat/venues/models.py | 4 +- 34 files changed, 1737 insertions(+), 917 deletions(-) create mode 100644 tabbycat/adjallocation/migrations/0010_alter_adjudicatoradjudicatorconflict_unique_together_and_more.py create mode 100644 tabbycat/adjfeedback/migrations/0015_alter_adjudicatorfeedback_unique_together_and_more.py create mode 100644 tabbycat/availability/migrations/0006_alter_roundavailability_unique_together_and_more.py create mode 100644 tabbycat/breakqual/migrations/0006_alter_breakcategory_unique_together_and_more.py create mode 100644 tabbycat/draw/migrations/0009_alter_teamsideallocation_unique_together_and_more.py create mode 100644 tabbycat/motions/migrations/0006_alter_debateteammotionpreference_unique_together_and_more.py create mode 100644 tabbycat/notifications/migrations/0013_alter_bulknotification_event.py create mode 100644 tabbycat/participants/migrations/0022_rename_team_tournament_institution_short_reference_participant_tournam_160efa_idx_and_more.py create mode 100644 tabbycat/participants/migrations/0023_alter_institution_unique_together_and_more.py create mode 100644 tabbycat/results/migrations/0016_rename_speakerscorebyadj_ballot_submission_debate_adjudicator_results_spe_ballot__667598_idx_and_mor.py create mode 100644 tabbycat/tournaments/migrations/0011_alter_round_unique_together_and_more.py create mode 100644 tabbycat/utils/models.py create mode 100644 tabbycat/venues/migrations/0011_alter_venue_options_alter_venue_index_together_and_more.py diff --git a/.python-version b/.python-version index 78c9a28efce..2419ad5b0a3 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.9.12 +3.11.9 diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 5a812c75e8a..d81bb1045f8 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -9,7 +9,7 @@ version: 2 build: os: ubuntu-20.04 tools: - python: "3.9" + python: "3.12" # Build documentation in the docs/ directory with Sphinx sphinx: diff --git a/Dockerfile b/Dockerfile index 36972f882c3..aad7d5e7ac9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # run the application specified. docker-compose does not use this. # Grab a python image -FROM python:3.9 +FROM python:3.11 SHELL ["/bin/bash", "--login", "-c"] # Just needed for all things python (note this is setting an env variable) diff --git a/Pipfile b/Pipfile index 2b411cce45c..bb442791d89 100644 --- a/Pipfile +++ b/Pipfile @@ -51,4 +51,4 @@ sphinx-rtd-theme = "*" sphinx-intl = "*" [requires] -python_version = "3.9" +python_version = "3.11" diff --git a/Pipfile.lock b/Pipfile.lock index 7de712ca7db..ac8af7ac321 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "ed77cfd488f49a5f8d7dc88cca9ba162c39a55425e0335847cf91ff7c43cb51a" + "sha256": "2c6c7279511a59524494d4ae59fb78ac13efe891c03b28c26ea8c01ce6975a4d" }, "pipfile-spec": 6, "requires": { - "python_version": "3.9" + "python_version": "3.11" }, "sources": [ { @@ -18,42 +18,35 @@ "default": { "appnope": { "hashes": [ - "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24", - "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e" + "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", + "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c" ], "markers": "sys_platform == 'darwin'", - "version": "==0.1.3" + "version": "==0.1.4" }, "asgiref": { "hashes": [ - "sha256:71e68008da809b957b7ee4b43dbccff33d1b23519fb8344e33f049897077afac", - "sha256:9567dfe7bd8d3c8c892227827c41cce860b368104c3431da67a0c5a65a949506" + "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", + "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590" ], "index": "pypi", - "version": "==3.6.0" - }, - "async-timeout": { - "hashes": [ - "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15", - "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c" - ], - "markers": "python_version >= '3.6'", - "version": "==4.0.2" + "markers": "python_version >= '3.8'", + "version": "==3.8.1" }, "attrs": { "hashes": [ - "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836", - "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99" + "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", + "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" ], - "markers": "python_version >= '3.6'", - "version": "==22.2.0" + "markers": "python_version >= '3.7'", + "version": "==23.2.0" }, "autobahn": { "hashes": [ - "sha256:c5ef8ca7422015a1af774a883b8aef73d4954c9fcd182c9b5244e08e973f7c3a" + "sha256:ec9421c52a2103364d1ef0468036e6019ee84f71721e86b36fe19ad6966c1181" ], - "markers": "python_version >= '3.7'", - "version": "==23.1.2" + "markers": "python_version >= '3.9'", + "version": "==23.6.2" }, "automat": { "hashes": [ @@ -71,148 +64,150 @@ }, "bleach": { "hashes": [ - "sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414", - "sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4" + "sha256:0a31f1837963c41d46bbf1331b8778e1308ea0791db03cc4e7357b97cf42a8fe", + "sha256:3225f354cfc436b9789c66c4ee030194bee0568fbf9cbdad3bc8b5c26c5f12b6" ], - "markers": "python_version >= '3.7'", - "version": "==6.0.0" + "markers": "python_version >= '3.8'", + "version": "==6.1.0" }, "certifi": { "hashes": [ - "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", - "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" + "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", + "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" ], "markers": "python_version >= '3.6'", - "version": "==2022.12.7" + "version": "==2024.2.2" }, "cffi": { "hashes": [ - "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", - "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", - "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", - "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", - "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405", - "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", - "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", - "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", - "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", - "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf", - "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", - "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497", - "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", - "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", - "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", - "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", - "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", - "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", - "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", - "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", - "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", - "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", - "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a", - "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2", - "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", - "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7", - "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", - "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", - "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", - "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", - "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", - "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", - "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e", - "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", - "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", - "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", - "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", - "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", - "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", - "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", - "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", - "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", - "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914", - "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", - "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", - "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", - "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", - "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2", - "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", - "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3", - "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", - "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", - "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", - "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d", - "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", - "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162", - "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", - "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", - "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e", - "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", - "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6", - "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b", - "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", - "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0" - ], - "version": "==1.15.1" + "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc", + "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a", + "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417", + "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab", + "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520", + "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36", + "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743", + "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8", + "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed", + "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684", + "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56", + "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324", + "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d", + "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235", + "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e", + "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088", + "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000", + "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7", + "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e", + "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673", + "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c", + "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe", + "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2", + "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098", + "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8", + "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a", + "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0", + "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b", + "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896", + "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e", + "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9", + "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2", + "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b", + "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6", + "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404", + "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f", + "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0", + "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4", + "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc", + "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936", + "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba", + "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872", + "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb", + "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614", + "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1", + "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d", + "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969", + "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b", + "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4", + "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627", + "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956", + "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357" + ], + "markers": "platform_python_implementation != 'PyPy'", + "version": "==1.16.0" }, "channels": { "hashes": [ - "sha256:0ce53507a7da7b148eaa454526e0e05f7da5e5d1c23440e4886cf146981d8420", - "sha256:2253334ac76f67cba68c2072273f7e0e67dbdac77eeb7e318f511d2f9a53c5e4" + "sha256:a3c4419307f582c3f71d67bfb6eff748ae819c2f360b9b141694d84f242baa48", + "sha256:e0ed375719f5c1851861f05ed4ce78b0166f9245ca0ecd836cb77d4bb531489d" ], "index": "pypi", - "version": "==4.0.0" + "markers": "python_version >= '3.8'", + "version": "==4.1.0" }, "channels-redis": { "hashes": [ - "sha256:122414f29f525f7b9e0c9d59cdcfc4dc1b0eecba16fbb6a1c23f1d9b58f49dcb", - "sha256:81b59d68f53313e1aa891f23591841b684abb936b42e4d1a966d9e4dc63a95ec" + "sha256:01c26c4d5d3a203f104bba9e5585c0305a70df390d21792386586068162027fd", + "sha256:2c5b944a39bd984b72aa8005a3ae11637bf29b5092adeb91c9aad4ab819a8ac4" ], "index": "pypi", - "version": "==4.0.0" + "markers": "python_version >= '3.8'", + "version": "==4.2.0" }, "constantly": { "hashes": [ - "sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35", - "sha256:dd2fa9d6b1a51a83f0d7dd76293d734046aa176e384bf6e33b7e44880eb37c5d" + "sha256:3fd9b4d1c3dc1ec9757f3c52aef7e53ad9323dbe39f51dfd4c43853b68dfa3f9", + "sha256:aa92b70a33e2ac0bb33cd745eb61776594dc48764b06c35e0efd050b7f1c7cbd" ], - "version": "==15.1.0" + "markers": "python_version >= '3.8'", + "version": "==23.10.4" }, "cryptography": { "hashes": [ - "sha256:103e8f7155f3ce2ffa0049fe60169878d47a4364b277906386f8de21c9234aa1", - "sha256:23df8ca3f24699167daf3e23e51f7ba7334d504af63a94af468f468b975b7dd7", - "sha256:2725672bb53bb92dc7b4150d233cd4b8c59615cd8288d495eaa86db00d4e5c06", - "sha256:30b1d1bfd00f6fc80d11300a29f1d8ab2b8d9febb6ed4a38a76880ec564fae84", - "sha256:35d658536b0a4117c885728d1a7032bdc9a5974722ae298d6c533755a6ee3915", - "sha256:50cadb9b2f961757e712a9737ef33d89b8190c3ea34d0fb6675e00edbe35d074", - "sha256:5f8c682e736513db7d04349b4f6693690170f95aac449c56f97415c6980edef5", - "sha256:6236a9610c912b129610eb1a274bdc1350b5df834d124fa84729ebeaf7da42c3", - "sha256:788b3921d763ee35dfdb04248d0e3de11e3ca8eb22e2e48fef880c42e1f3c8f9", - "sha256:8bc0008ef798231fac03fe7d26e82d601d15bd16f3afaad1c6113771566570f3", - "sha256:8f35c17bd4faed2bc7797d2a66cbb4f986242ce2e30340ab832e5d99ae60e011", - "sha256:b49a88ff802e1993b7f749b1eeb31134f03c8d5c956e3c125c75558955cda536", - "sha256:bc0521cce2c1d541634b19f3ac661d7a64f9555135e9d8af3980965be717fd4a", - "sha256:bc5b871e977c8ee5a1bbc42fa8d19bcc08baf0c51cbf1586b0e87a2694dde42f", - "sha256:c43ac224aabcbf83a947eeb8b17eaf1547bce3767ee2d70093b461f31729a480", - "sha256:d15809e0dbdad486f4ad0979753518f47980020b7a34e9fc56e8be4f60702fac", - "sha256:d7d84a512a59f4412ca8549b01f94be4161c94efc598bf09d027d67826beddc0", - "sha256:e029b844c21116564b8b61216befabca4b500e6816fa9f0ba49527653cae2108", - "sha256:e8a0772016feeb106efd28d4a328e77dc2edae84dfbac06061319fdb669ff828", - "sha256:e944fe07b6f229f4c1a06a7ef906a19652bdd9fd54c761b0ff87e83ae7a30354", - "sha256:eb40fe69cfc6f5cdab9a5ebd022131ba21453cf7b8a7fd3631f45bbf52bed612", - "sha256:fa507318e427169ade4e9eccef39e9011cdc19534f55ca2f36ec3f388c1f70f3", - "sha256:ffd394c7896ed7821a6d13b24657c6a34b6e2650bd84ae063cf11ccffa4f1a97" + "sha256:00c0faa5b021457848d031ecff041262211cc1e2bce5f6e6e6c8108018f6b44a", + "sha256:073104df012fc815eed976cd7d0a386c8725d0d0947cf9c37f6c36a6c20feb1b", + "sha256:076c92b08dd1ab88108bc84545187e10d3693a9299c593f98c4ea195a0b0ead7", + "sha256:089aeb297ff89615934b22c7631448598495ffd775b7d540a55cfee35a677bf4", + "sha256:3b750279f3e7715df6f68050707a0cee7cbe81ba2eeb2f21d081bd205885ffed", + "sha256:43e521f21c2458038d72e8cdfd4d4d9f1d00906a7b6636c4272e35f650d1699b", + "sha256:4bdb39ecbf05626e4bfa1efd773bb10346af297af14fb3f4c7cb91a1d2f34a46", + "sha256:5967e3632f42b0c0f9dc2c9da88c79eabdda317860b246d1fbbde4a8bbbc3b44", + "sha256:65d529c31bd65d54ce6b926a01e1b66eacf770b7e87c0622516a840e400ec732", + "sha256:6981acac509cc9415344cb5bfea8130096ea6ebcc917e75503143a1e9e829160", + "sha256:81dbe47e28b703bc4711ac74a64ef8b758a0cf056ce81d08e39116ab4bc126fa", + "sha256:8b90c57b3cd6128e0863b894ce77bd36fcb5f430bf2377bc3678c2f56e232316", + "sha256:9184aff0856261ecb566a3eb26a05dfe13a292c85ce5c59b04e4aa09e5814187", + "sha256:945a43ebf036dd4b43ebfbbd6b0f2db29ad3d39df824fb77476ca5777a9dde33", + "sha256:97eeacae9aa526ddafe68b9202a535f581e21d78f16688a84c8dcc063618e121", + "sha256:9f1a3bc2747166b0643b00e0b56cd9b661afc9d5ff963acaac7a9c7b2b1ef638", + "sha256:9ff75b88a4d273c06d968ad535e6cb6a039dd32db54fe36f05ed62ac3ef64a44", + "sha256:aeb6f56b004e898df5530fa873e598ec78eb338ba35f6fa1449970800b1d97c2", + "sha256:b16b90605c62bcb3aa7755d62cf5e746828cfc3f965a65211849e00c46f8348d", + "sha256:b99831397fdc6e6e0aa088b060c278c6e635d25c0d4d14bdf045bf81792fda0a", + "sha256:bc954251edcd8a952eeaec8ae989fec7fe48109ab343138d537b7ea5bb41071a", + "sha256:c05230d8aaaa6b8ab3ab41394dc06eb3d916131df1c9dcb4c94e8f041f704b74", + "sha256:d16a310c770cc49908c500c2ceb011f2840674101a587d39fa3ea828915b7e83", + "sha256:d93080d2b01b292e7ee4d247bf93ed802b0100f5baa3fa5fd6d374716fa480d4", + "sha256:e1f5f15c5ddadf6ee4d1d624a2ae940f14bd74536230b0056ccb28bb6248e42a", + "sha256:e3442601d276bd9e961d618b799761b4e5d892f938e8a4fe1efbe2752be90455", + "sha256:e85f433230add2aa26b66d018e21134000067d210c9c68ef7544ba65fc52e3eb", + "sha256:eecca86813c6a923cabff284b82ff4d73d9e91241dc176250192c3a9b9902a54", + "sha256:f1e933b238978ccfa77b1fee0a297b3c04983f4cb84ae1c33b0ea4ae08266cc9", + "sha256:f4cece02478d73dacd52be57a521d168af64ae03d2a567c0c4eb6f189c3b9d79", + "sha256:f567a82b7c2b99257cca2a1c902c1b129787278ff67148f188784245c7ed5495", + "sha256:f987a244dfb0333fbd74a691c36000a2569eaf7c7cc2ac838f85f59f0588ddc9" ], - "markers": "python_version >= '3.6'", - "version": "==39.0.2" + "markers": "python_version >= '3.7'", + "version": "==42.0.6" }, "daphne": { "hashes": [ - "sha256:a288ece46012b6b719c37150be67c69ebfca0793a8521bf821533bad983179b2", - "sha256:cce9afc8f49a4f15d4270b8cfb0e0fe811b770a5cc795474e97e4da287497666" + "sha256:618d1322bb4d875342b99dd2a10da2d9aae7ee3645f765965fdc1e658ea5290a", + "sha256:fcbcace38eb86624ae247c7ffdc8ac12f155d7d19eafac4247381896d6f33761" ], "index": "pypi", - "version": "==4.0.0" + "markers": "python_version >= '3.8'", + "version": "==4.1.2" }, "decorator": { "hashes": [ @@ -228,6 +223,7 @@ "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61" ], "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==0.7.1" }, "dj-cmd": { @@ -240,27 +236,29 @@ }, "dj-database-url": { "hashes": [ - "sha256:5c2993b91801c0f78a8b19e642b497b90831124cbade0c265900d4c1037b4730", - "sha256:b23b15046cb38180e0c95207bcc90fe5e9dbde8eef16065907dd85cf4ca7036c" + "sha256:04bc34b248d4c21aaa13e4ab419ae6575ef5f10f3df735ce7da97722caa356e0", + "sha256:f2042cefe1086e539c9da39fad5ad7f61173bf79665e69bf7e4de55fa88b135f" ], "index": "pypi", - "version": "==1.2.0" + "version": "==2.1.0" }, "django": { "hashes": [ - "sha256:44f714b81c5f190d9d2ddad01a532fe502fa01c4cb8faf1d081f4264ed15dcd8", - "sha256:f2f431e75adc40039ace496ad3b9f17227022e8b11566f4b363da44c7e44761e" + "sha256:4bd01a8c830bb77a8a3b0e7d8b25b887e536ad17a81ba2dce5476135c73312bd", + "sha256:916423499d75d62da7aa038d19aef23d23498d8df229775eb0a6309ee1013775" ], "index": "pypi", - "version": "==4.1.7" + "markers": "python_version >= '3.10'", + "version": "==5.0.4" }, "django-appconf": { "hashes": [ - "sha256:ae9f864ee1958c815a965ed63b3fba4874eec13de10236ba063a788f9a17389d", - "sha256:be3db0be6c81fa84742000b89a81c016d70ae66a7ccb620cdef592b1f1a6aaa4" + "sha256:c3ae442fba1ff7ec830412c5184b17169a7a1e71cf0864a4c3f93cf4c98a1993", + "sha256:cfe87ea827c4ee04b9a70fab90b86d704cb02f2981f89da8423cb0fabf88efbf" ], "index": "pypi", - "version": "==1.0.5" + "markers": "python_version >= '3.7'", + "version": "==1.0.6" }, "django-better-admin-arrayfield": { "hashes": [ @@ -272,34 +270,38 @@ }, "django-cors-headers": { "hashes": [ - "sha256:5fbd58a6fb4119d975754b2bc090f35ec160a8373f276612c675b00e8a138739", - "sha256:684180013cc7277bdd8702b80a3c5a4b3fcae4abb2bf134dceb9f5dfe300228e" + "sha256:0b1fd19297e37417fc9f835d39e45c8c642938ddba1acce0c1753d3edef04f36", + "sha256:0bf65ef45e606aff1994d35503e6b677c0b26cafff6506f8fd7187f3be840207" ], "index": "pypi", - "version": "==3.14.0" + "markers": "python_version >= '3.8'", + "version": "==4.3.1" }, "django-dynamic-preferences": { "hashes": [ - "sha256:c00abcb8d524067390a66518cfcd32683b87ad3cc620d5913649fc7707b80833" + "sha256:0d3d456626244d0bdaf312c81f2b3e14bd16134e8fcf53a33fd12e5d0bdd88dd", + "sha256:527b943d2b5a5c1ea2de8f941778a39efddf2b18ccf44e6aaab05290256ec82f" ], "index": "pypi", - "version": "==1.14.0" + "version": "==1.16.0" }, "django-extensions": { "hashes": [ - "sha256:2a4f4d757be2563cd1ff7cfdf2e57468f5f931cc88b23cf82ca75717aae504a4", - "sha256:421464be390289513f86cb5e18eb43e5dc1de8b4c27ba9faa3b91261b0d67e09" + "sha256:44d27919d04e23b3f40231c4ab7af4e61ce832ef46d610cc650d53e68328410a", + "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401" ], "index": "pypi", - "version": "==3.2.1" + "markers": "python_version >= '3.6'", + "version": "==3.2.3" }, "django-formtools": { "hashes": [ - "sha256:deb932be55b1d9419e37dc4d65dfbfeb8d307b71c8c11fd52f159aba5fc0deed", - "sha256:f5f32f62ec8192cd1bc55bd929ca7dff5a5f2addf9027db95a5906ecfaa64836" + "sha256:47cb34552c6efca088863d693284d04fc36eaaf350eb21e1a1d935e0df523c93", + "sha256:bce9b64eda52cc1eef6961cc649cf75aacd1a707c2fff08d6c3efcbc8e7e761a" ], "index": "pypi", - "version": "==2.4" + "markers": "python_version >= '3.8'", + "version": "==2.5.1" }, "django-gfklookupwidget": { "hashes": [ @@ -310,18 +312,19 @@ }, "django-ipware": { "hashes": [ - "sha256:602a58325a4808bd19197fef2676a0b2da2df40d0ecf21be414b2ff48c72ad05", - "sha256:878dbb06a87e25550798e9ef3204ed70a200dd8b15e47dcef848cf08244f04c9" + "sha256:d9ec43d2bf7cdf216fed8d494a084deb5761a54860a53b2e74346a4f384cff47", + "sha256:db16bbee920f661ae7f678e4270460c85850f03c6761a4eaeb489bdc91f64709" ], "index": "pypi", - "version": "==4.0.2" + "markers": "python_version >= '3.8'", + "version": "==7.0.1" }, "django-jet-reboot": { "hashes": [ - "sha256:87263acbbbd56b5e92c98987d2e8132034d65261473cceb8cca57dafaab85b87" + "sha256:e1cc40606331539106cae58701bbaf76c0a4486bbe1aee6d85b2f20ce8ad3f71" ], "index": "pypi", - "version": "==1.3.3" + "version": "==1.3.7" }, "django-polymorphic": { "hashes": [ @@ -333,27 +336,29 @@ }, "django-redis": { "hashes": [ - "sha256:1d037dc02b11ad7aa11f655d26dac3fb1af32630f61ef4428860a2e29ff92026", - "sha256:8a99e5582c79f894168f5865c52bd921213253b7fd64d16733ae4591564465de" + "sha256:6a02abaa34b0fea8bf9b707d2c363ab6adc7409950b2db93602e6cb292818c42", + "sha256:ebc88df7da810732e2af9987f7f426c96204bf89319df4c6da6ca9a2942edd5b" ], "index": "pypi", - "version": "==5.2.0" + "markers": "python_version >= '3.6'", + "version": "==5.4.0" }, "django-split-settings": { "hashes": [ - "sha256:31415a618256b54c5cee8662cbaa72a890683b8b7465d64ba88fdd3affdd6c60", - "sha256:4b3be146776d49c61bd9dcf89fad40edb1544f13ab27a87a0b1aecf5a0d636f4" + "sha256:c1f57f6b54fc0d93082c12163e76fad082c214f5fa0d16d84a1226d2c9f14f26", + "sha256:c902ef60d5fe8190ff224284f68e3c9015b6f1aca9e9d6bd70bf86394ff32634" ], "index": "pypi", - "version": "==1.2.0" + "markers": "python_version >= '3.9' and python_version < '4.0'", + "version": "==1.3.1" }, "django-statici18n": { "hashes": [ - "sha256:00079579035d5b45320830191e2c047f8643b7906307eff9833f0fa95068a603", - "sha256:5f4bb3d58670def2df490babe338524927cfb2ebe2e5e20538b98d9424e83d0e" + "sha256:323ebecbfa39408ad242a5e782083bac73bf0e13d93c3e2b29fd32d3379eb3c8", + "sha256:80ac9f21cb80c1cc5a60b558a104b47acc0914fe2bae9fef74f5a0100c8f3d36" ], "index": "pypi", - "version": "==2.3.1" + "version": "==2.5.0" }, "django-summernote": { "hashes": [ @@ -364,11 +369,12 @@ }, "djangorestframework": { "hashes": [ - "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8", - "sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08" + "sha256:3ccc0475bce968608cf30d07fb17d8e52d1d7fc8bfe779c905463200750cbca6", + "sha256:f88fad74183dfc7144b2756d0d2ac716ea5b4c7c9840995ac3bfd8ec034333c1" ], "index": "pypi", - "version": "==3.14.0" + "markers": "python_version >= '3.6'", + "version": "==3.15.1" }, "drf-link-header-pagination": { "hashes": [ @@ -376,23 +382,26 @@ "sha256:9890a871803395544c5a0ee0665fc2d2701fcdf151570ed15c8b8d873aefe27d" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==0.2.0" }, "drf-spectacular": { "hashes": [ - "sha256:789696f9845ef2397c52f66154aec6d96411baf6aa09a5d40c5f0b0e99f6b3d8", - "sha256:d58684e702f5ad436c5bd1735d46df0e123e64de883092d38f1debb9fa4a03c9" + "sha256:a199492f2163c4101055075ebdbb037d59c6e0030692fc83a1a8c0fc65929981", + "sha256:b1c04bf8b2fbbeaf6f59414b4ea448c8787aba4d32f76055c3b13335cf7ec37b" ], "index": "pypi", - "version": "==0.25.1" + "markers": "python_version >= '3.7'", + "version": "==0.27.2" }, "gunicorn": { "hashes": [ - "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e", - "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8" + "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9", + "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63" ], "index": "pypi", - "version": "==20.1.0" + "markers": "python_version >= '3.7'", + "version": "==22.0.0" }, "honcho": { "hashes": [ @@ -404,11 +413,11 @@ }, "html2text": { "hashes": [ - "sha256:c7c629882da0cf377d66f073329ccf34a12ed2adf0169b9285ae4e63ef54c82b", - "sha256:e296318e16b059ddb97f7a8a1d6a5c1d7af4544049a01e261731d2d5cc277bbb" + "sha256:05f8e367d15aaabc96415376776cdd11afd5127a77fce6e36afc60c563ca2c32" ], "index": "pypi", - "version": "==2020.1.16" + "markers": "python_version >= '3.8'", + "version": "==2024.2.26" }, "hyperlink": { "hashes": [ @@ -419,10 +428,10 @@ }, "idna": { "hashes": [ - "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", - "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", + "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" ], - "version": "==3.4" + "version": "==3.7" }, "incremental": { "hashes": [ @@ -445,88 +454,102 @@ "sha256:c175d2440a1caff76116eb719d40538fbb316e214eda85c5515c303aacbfb23e" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==7.34.0" }, "jedi": { "hashes": [ - "sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e", - "sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612" + "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd", + "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0" ], "markers": "python_version >= '3.6'", - "version": "==0.18.2" + "version": "==0.19.1" }, "jsonschema": { "hashes": [ - "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d", - "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6" + "sha256:5b22d434a45935119af990552c862e5d6d564e8f6601206b305a61fdf661a2b7", + "sha256:ff4cfd6b1367a40e7bc6411caec72effadd3db0bbe5017de188f2d6108335802" ], - "markers": "python_version >= '3.7'", - "version": "==4.17.3" + "markers": "python_version >= '3.8'", + "version": "==4.22.0" + }, + "jsonschema-specifications": { + "hashes": [ + "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc", + "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c" + ], + "markers": "python_version >= '3.8'", + "version": "==2023.12.1" }, "matplotlib-inline": { "hashes": [ - "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311", - "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304" + "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", + "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca" ], - "markers": "python_version >= '3.5'", - "version": "==0.1.6" + "markers": "python_version >= '3.8'", + "version": "==0.1.7" }, "msgpack": { "hashes": [ - "sha256:002b5c72b6cd9b4bafd790f364b8480e859b4712e91f43014fe01e4f957b8467", - "sha256:0a68d3ac0104e2d3510de90a1091720157c319ceeb90d74f7b5295a6bee51bae", - "sha256:0df96d6eaf45ceca04b3f3b4b111b86b33785683d682c655063ef8057d61fd92", - "sha256:0dfe3947db5fb9ce52aaea6ca28112a170db9eae75adf9339a1aec434dc954ef", - "sha256:0e3590f9fb9f7fbc36df366267870e77269c03172d086fa76bb4eba8b2b46624", - "sha256:11184bc7e56fd74c00ead4f9cc9a3091d62ecb96e97653add7a879a14b003227", - "sha256:112b0f93202d7c0fef0b7810d465fde23c746a2d482e1e2de2aafd2ce1492c88", - "sha256:1276e8f34e139aeff1c77a3cefb295598b504ac5314d32c8c3d54d24fadb94c9", - "sha256:1576bd97527a93c44fa856770197dec00d223b0b9f36ef03f65bac60197cedf8", - "sha256:1e91d641d2bfe91ba4c52039adc5bccf27c335356055825c7f88742c8bb900dd", - "sha256:26b8feaca40a90cbe031b03d82b2898bf560027160d3eae1423f4a67654ec5d6", - "sha256:2999623886c5c02deefe156e8f869c3b0aaeba14bfc50aa2486a0415178fce55", - "sha256:2a2df1b55a78eb5f5b7d2a4bb221cd8363913830145fad05374a80bf0877cb1e", - "sha256:2bb8cdf50dd623392fa75525cce44a65a12a00c98e1e37bf0fb08ddce2ff60d2", - "sha256:2cc5ca2712ac0003bcb625c96368fd08a0f86bbc1a5578802512d87bc592fe44", - "sha256:35bc0faa494b0f1d851fd29129b2575b2e26d41d177caacd4206d81502d4c6a6", - "sha256:3c11a48cf5e59026ad7cb0dc29e29a01b5a66a3e333dc11c04f7e991fc5510a9", - "sha256:449e57cc1ff18d3b444eb554e44613cffcccb32805d16726a5494038c3b93dab", - "sha256:462497af5fd4e0edbb1559c352ad84f6c577ffbbb708566a0abaaa84acd9f3ae", - "sha256:4733359808c56d5d7756628736061c432ded018e7a1dff2d35a02439043321aa", - "sha256:48f5d88c99f64c456413d74a975bd605a9b0526293218a3b77220a2c15458ba9", - "sha256:49565b0e3d7896d9ea71d9095df15b7f75a035c49be733051c34762ca95bbf7e", - "sha256:4ab251d229d10498e9a2f3b1e68ef64cb393394ec477e3370c457f9430ce9250", - "sha256:4d5834a2a48965a349da1c5a79760d94a1a0172fbb5ab6b5b33cbf8447e109ce", - "sha256:4dea20515f660aa6b7e964433b1808d098dcfcabbebeaaad240d11f909298075", - "sha256:545e3cf0cf74f3e48b470f68ed19551ae6f9722814ea969305794645da091236", - "sha256:63e29d6e8c9ca22b21846234913c3466b7e4ee6e422f205a2988083de3b08cae", - "sha256:6916c78f33602ecf0509cc40379271ba0f9ab572b066bd4bdafd7434dee4bc6e", - "sha256:6a4192b1ab40f8dca3f2877b70e63799d95c62c068c84dc028b40a6cb03ccd0f", - "sha256:6c9566f2c39ccced0a38d37c26cc3570983b97833c365a6044edef3574a00c08", - "sha256:76ee788122de3a68a02ed6f3a16bbcd97bc7c2e39bd4d94be2f1821e7c4a64e6", - "sha256:7760f85956c415578c17edb39eed99f9181a48375b0d4a94076d84148cf67b2d", - "sha256:77ccd2af37f3db0ea59fb280fa2165bf1b096510ba9fe0cc2bf8fa92a22fdb43", - "sha256:81fc7ba725464651190b196f3cd848e8553d4d510114a954681fd0b9c479d7e1", - "sha256:85f279d88d8e833ec015650fd15ae5eddce0791e1e8a59165318f371158efec6", - "sha256:9667bdfdf523c40d2511f0e98a6c9d3603be6b371ae9a238b7ef2dc4e7a427b0", - "sha256:a75dfb03f8b06f4ab093dafe3ddcc2d633259e6c3f74bb1b01996f5d8aa5868c", - "sha256:ac5bd7901487c4a1dd51a8c58f2632b15d838d07ceedaa5e4c080f7190925bff", - "sha256:aca0f1644d6b5a73eb3e74d4d64d5d8c6c3d577e753a04c9e9c87d07692c58db", - "sha256:b17be2478b622939e39b816e0aa8242611cc8d3583d1cd8ec31b249f04623243", - "sha256:c1683841cd4fa45ac427c18854c3ec3cd9b681694caf5bff04edb9387602d661", - "sha256:c23080fdeec4716aede32b4e0ef7e213c7b1093eede9ee010949f2a418ced6ba", - "sha256:d5b5b962221fa2c5d3a7f8133f9abffc114fe218eb4365e40f17732ade576c8e", - "sha256:d603de2b8d2ea3f3bcb2efe286849aa7a81531abc52d8454da12f46235092bcb", - "sha256:e83f80a7fec1a62cf4e6c9a660e39c7f878f603737a0cdac8c13131d11d97f52", - "sha256:eb514ad14edf07a1dbe63761fd30f89ae79b42625731e1ccf5e1f1092950eaa6", - "sha256:eba96145051ccec0ec86611fe9cf693ce55f2a3ce89c06ed307de0e085730ec1", - "sha256:ed6f7b854a823ea44cf94919ba3f727e230da29feb4a99711433f25800cf747f", - "sha256:f0029245c51fd9473dc1aede1160b0a29f4a912e6b1dd353fa6d317085b219da", - "sha256:f5d869c18f030202eb412f08b28d2afeea553d6613aee89e200d7aca7ef01f5f", - "sha256:fb62ea4b62bfcb0b380d5680f9a4b3f9a2d166d9394e9bbd9666c0ee09a3645c", - "sha256:fcb8a47f43acc113e24e910399376f7277cf8508b27e5b88499f053de6b115a8" - ], - "version": "==1.0.4" + "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982", + "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3", + "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40", + "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee", + "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693", + "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950", + "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151", + "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24", + "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305", + "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b", + "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c", + "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659", + "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d", + "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18", + "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746", + "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868", + "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2", + "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba", + "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228", + "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2", + "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273", + "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c", + "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653", + "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a", + "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596", + "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd", + "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8", + "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa", + "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85", + "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc", + "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836", + "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3", + "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58", + "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128", + "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db", + "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f", + "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77", + "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad", + "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13", + "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8", + "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b", + "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a", + "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543", + "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b", + "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce", + "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d", + "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a", + "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c", + "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f", + "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e", + "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011", + "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04", + "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480", + "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a", + "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d", + "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d" + ], + "markers": "python_version >= '3.8'", + "version": "==1.0.8" }, "munkres": { "hashes": [ @@ -538,19 +561,28 @@ }, "networkx": { "hashes": [ - "sha256:58058d66b1818043527244fab9d41a51fcd7dcc271748015f3c181b8a90c8e2e", - "sha256:9a9992345353618ae98339c2b63d8201c381c2944f38a2ab49cb45a4c667e412" + "sha256:0c127d8b2f4865f59ae9cb8aafcd60b5c70f3241ebd66f7defad7c4ab90126c9", + "sha256:28575580c6ebdaf4505b22c6256a2b9de86b316dc63ba9e93abde3d78dfdbcf2" ], "index": "pypi", - "version": "==3.0" + "markers": "python_version >= '3.10'", + "version": "==3.3" + }, + "packaging": { + "hashes": [ + "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" + ], + "markers": "python_version >= '3.7'", + "version": "==24.0" }, "parso": { "hashes": [ - "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0", - "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75" + "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", + "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d" ], "markers": "python_version >= '3.6'", - "version": "==0.8.3" + "version": "==0.8.4" }, "persisting-theory": { "hashes": [ @@ -561,11 +593,11 @@ }, "pexpect": { "hashes": [ - "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937", - "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c" + "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", + "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f" ], "markers": "sys_platform != 'win32'", - "version": "==4.8.0" + "version": "==4.9.0" }, "pickleshare": { "hashes": [ @@ -576,88 +608,90 @@ }, "prompt-toolkit": { "hashes": [ - "sha256:23ac5d50538a9a38c8bde05fecb47d0b403ecd0662857a86f886f798563d5b9b", - "sha256:45ea77a2f7c60418850331366c81cf6b5b9cf4c7fd34616f733c5427e6abbb1f" + "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d", + "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6" ], - "markers": "python_version >= '3.7'", - "version": "==3.0.38" + "markers": "python_full_version >= '3.7.0'", + "version": "==3.0.43" }, "psycopg2-binary": { "hashes": [ - "sha256:00475004e5ed3e3bf5e056d66e5dcdf41a0dc62efcd57997acd9135c40a08a50", - "sha256:01ad49d68dd8c5362e4bfb4158f2896dc6e0c02e87b8a3770fc003459f1a4425", - "sha256:024030b13bdcbd53d8a93891a2cf07719715724fc9fee40243f3bd78b4264b8f", - "sha256:02551647542f2bf89073d129c73c05a25c372fc0a49aa50e0de65c3c143d8bd0", - "sha256:043a9fd45a03858ff72364b4b75090679bd875ee44df9c0613dc862ca6b98460", - "sha256:05b3d479425e047c848b9782cd7aac9c6727ce23181eb9647baf64ffdfc3da41", - "sha256:0775d6252ccb22b15da3b5d7adbbf8cfe284916b14b6dc0ff503a23edb01ee85", - "sha256:1764546ffeaed4f9428707be61d68972eb5ede81239b46a45843e0071104d0dd", - "sha256:1e491e6489a6cb1d079df8eaa15957c277fdedb102b6a68cfbf40c4994412fd0", - "sha256:212757ffcecb3e1a5338d4e6761bf9c04f750e7d027117e74aa3cd8a75bb6fbd", - "sha256:215d6bf7e66732a514f47614f828d8c0aaac9a648c46a831955cb103473c7147", - "sha256:25382c7d174c679ce6927c16b6fbb68b10e56ee44b1acb40671e02d29f2fce7c", - "sha256:2abccab84d057723d2ca8f99ff7b619285d40da6814d50366f61f0fc385c3903", - "sha256:2d964eb24c8b021623df1c93c626671420c6efadbdb8655cb2bd5e0c6fa422ba", - "sha256:2ec46ed947801652c9643e0b1dc334cfb2781232e375ba97312c2fc256597632", - "sha256:2ef892cabdccefe577088a79580301f09f2a713eb239f4f9f62b2b29cafb0577", - "sha256:33e632d0885b95a8b97165899006c40e9ecdc634a529dca7b991eb7de4ece41c", - "sha256:3520d7af1ebc838cc6084a3281145d5cd5bdd43fdef139e6db5af01b92596cb7", - "sha256:3d790f84201c3698d1bfb404c917f36e40531577a6dda02e45ba29b64d539867", - "sha256:3fc33295cfccad697a97a76dec3f1e94ad848b7b163c3228c1636977966b51e2", - "sha256:422e3d43b47ac20141bc84b3d342eead8d8099a62881a501e97d15f6addabfe9", - "sha256:426c2ae999135d64e6a18849a7d1ad0e1bd007277e4a8f4752eaa40a96b550ff", - "sha256:46512486be6fbceef51d7660dec017394ba3e170299d1dc30928cbedebbf103a", - "sha256:46850a640df62ae940e34a163f72e26aca1f88e2da79148e1862faaac985c302", - "sha256:484405b883630f3e74ed32041a87456c5e0e63a8e3429aa93e8714c366d62bd1", - "sha256:4e7904d1920c0c89105c0517dc7e3f5c20fb4e56ba9cdef13048db76947f1d79", - "sha256:56b2957a145f816726b109ee3d4e6822c23f919a7d91af5a94593723ed667835", - "sha256:5c6527c8efa5226a9e787507652dd5ba97b62d29b53c371a85cd13f957fe4d42", - "sha256:5cbc554ba47ecca8cd3396ddaca85e1ecfe3e48dd57dc5e415e59551affe568e", - "sha256:5d28ecdf191db558d0c07d0f16524ee9d67896edf2b7990eea800abeb23ebd61", - "sha256:5fc447058d083b8c6ac076fc26b446d44f0145308465d745fba93a28c14c9e32", - "sha256:63e318dbe52709ed10d516a356f22a635e07a2e34c68145484ed96a19b0c4c68", - "sha256:68d81a2fe184030aa0c5c11e518292e15d342a667184d91e30644c9d533e53e1", - "sha256:6e63814ec71db9bdb42905c925639f319c80e7909fb76c3b84edc79dadef8d60", - "sha256:6f8a9bcab7b6db2e3dbf65b214dfc795b4c6b3bb3af922901b6a67f7cb47d5f8", - "sha256:70831e03bd53702c941da1a1ad36c17d825a24fbb26857b40913d58df82ec18b", - "sha256:74eddec4537ab1f701a1647214734bc52cee2794df748f6ae5908e00771f180a", - "sha256:7b3751857da3e224f5629400736a7b11e940b5da5f95fa631d86219a1beaafec", - "sha256:7cf1d44e710ca3a9ce952bda2855830fe9f9017ed6259e01fcd71ea6287565f5", - "sha256:7d07f552d1e412f4b4e64ce386d4c777a41da3b33f7098b6219012ba534fb2c2", - "sha256:7d88db096fa19d94f433420eaaf9f3c45382da2dd014b93e4bf3215639047c16", - "sha256:7ee3095d02d6f38bd7d9a5358fcc9ea78fcdb7176921528dd709cc63f40184f5", - "sha256:902844f9c4fb19b17dfa84d9e2ca053d4a4ba265723d62ea5c9c26b38e0aa1e6", - "sha256:937880290775033a743f4836aa253087b85e62784b63fd099ee725d567a48aa1", - "sha256:95076399ec3b27a8f7fa1cc9a83417b1c920d55cf7a97f718a94efbb96c7f503", - "sha256:9c38d3869238e9d3409239bc05bc27d6b7c99c2a460ea337d2814b35fb4fea1b", - "sha256:9e32cedc389bcb76d9f24ea8a012b3cb8385ee362ea437e1d012ffaed106c17d", - "sha256:9ffdc51001136b699f9563b1c74cc1f8c07f66ef7219beb6417a4c8aaa896c28", - "sha256:a0adef094c49f242122bb145c3c8af442070dc0e4312db17e49058c1702606d4", - "sha256:a36a0e791805aa136e9cbd0ffa040d09adec8610453ee8a753f23481a0057af5", - "sha256:a7e518a0911c50f60313cb9e74a169a65b5d293770db4770ebf004245f24b5c5", - "sha256:af0516e1711995cb08dc19bbd05bec7dbdebf4185f68870595156718d237df3e", - "sha256:b8104f709590fff72af801e916817560dbe1698028cd0afe5a52d75ceb1fce5f", - "sha256:b911dfb727e247340d36ae20c4b9259e4a64013ab9888ccb3cbba69b77fd9636", - "sha256:b9a794cef1d9c1772b94a72eec6da144c18e18041d294a9ab47669bc77a80c1d", - "sha256:b9c33d4aef08dfecbd1736ceab8b7b3c4358bf10a0121483e5cd60d3d308cc64", - "sha256:b9d38a4656e4e715d637abdf7296e98d6267df0cc0a8e9a016f8ba07e4aa3eeb", - "sha256:bcda1c84a1c533c528356da5490d464a139b6e84eb77cc0b432e38c5c6dd7882", - "sha256:bef7e3f9dc6f0c13afdd671008534be5744e0e682fb851584c8c3a025ec09720", - "sha256:c15ba5982c177bc4b23a7940c7e4394197e2d6a424a2d282e7c236b66da6d896", - "sha256:c5254cbd4f4855e11cebf678c1a848a3042d455a22a4ce61349c36aafd4c2267", - "sha256:c5682a45df7d9642eff590abc73157c887a68f016df0a8ad722dcc0f888f56d7", - "sha256:c5e65c6ac0ae4bf5bef1667029f81010b6017795dcb817ba5c7b8a8d61fab76f", - "sha256:d4c7b3a31502184e856df1f7bbb2c3735a05a8ce0ade34c5277e1577738a5c91", - "sha256:d892bfa1d023c3781a3cab8dd5af76b626c483484d782e8bd047c180db590e4c", - "sha256:dbc332beaf8492b5731229a881807cd7b91b50dbbbaf7fe2faf46942eda64a24", - "sha256:dc85b3777068ed30aff8242be2813038a929f2084f69e43ef869daddae50f6ee", - "sha256:e59137cdb970249ae60be2a49774c6dfb015bd0403f05af1fe61862e9626642d", - "sha256:e67b3c26e9b6d37b370c83aa790bbc121775c57bfb096c2e77eacca25fd0233b", - "sha256:e72c91bda9880f097c8aa3601a2c0de6c708763ba8128006151f496ca9065935", - "sha256:f95b8aca2703d6a30249f83f4fe6a9abf2e627aa892a5caaab2267d56be7ab69" + "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9", + "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77", + "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e", + "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84", + "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3", + "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2", + "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67", + "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876", + "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152", + "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f", + "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a", + "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6", + "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503", + "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f", + "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493", + "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996", + "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f", + "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e", + "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59", + "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94", + "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7", + "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682", + "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420", + "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae", + "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291", + "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe", + "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980", + "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93", + "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692", + "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119", + "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716", + "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472", + "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b", + "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2", + "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc", + "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c", + "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5", + "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab", + "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984", + "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9", + "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf", + "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0", + "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f", + "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212", + "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb", + "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be", + "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90", + "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041", + "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7", + "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860", + "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d", + "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245", + "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27", + "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417", + "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359", + "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202", + "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0", + "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7", + "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba", + "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1", + "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd", + "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07", + "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98", + "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55", + "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d", + "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972", + "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f", + "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e", + "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26", + "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957", + "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53", + "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52" ], "index": "pypi", - "version": "==2.9.5" + "markers": "python_version >= '3.7'", + "version": "==2.9.9" }, "ptyprocess": { "hashes": [ @@ -668,61 +702,42 @@ }, "pyasn1": { "hashes": [ - "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", - "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576", - "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", - "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", - "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", - "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", - "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", - "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86", - "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12", - "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776", - "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", - "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2", - "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3" - ], - "version": "==0.4.8" + "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c", + "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473" + ], + "markers": "python_version >= '3.8'", + "version": "==0.6.0" }, "pyasn1-modules": { "hashes": [ - "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8", - "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199", - "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811", - "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed", - "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4", - "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", - "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74", - "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb", - "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45", - "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd", - "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0", - "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d", - "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405" - ], - "version": "==0.2.8" + "sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6", + "sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b" + ], + "markers": "python_version >= '3.8'", + "version": "==0.4.0" }, "pycparser": { "hashes": [ - "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", - "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", + "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" ], - "version": "==2.21" + "markers": "python_version >= '3.8'", + "version": "==2.22" }, "pygments": { "hashes": [ - "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297", - "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717" + "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", + "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a" ], - "markers": "python_version >= '3.6'", - "version": "==2.14.0" + "markers": "python_version >= '3.8'", + "version": "==2.18.0" }, "pyopenssl": { "hashes": [ - "sha256:c1cc5f86bcacefc84dada7d31175cae1b1518d5f60d3d0bb595a67822a868a6f", - "sha256:df5fc28af899e74e19fccb5510df423581047e10ab6f1f4ba1763ff5fde844c0" + "sha256:17ed5be5936449c5418d1cd269a1a9e9081bc54c17aed272b45856a3d3dc86ad", + "sha256:cabed4bfaa5df9f1a16c0ef64a0cb65318b5cd077a7eda7d6970131ca2f41a6f" ], - "version": "==23.0.0" + "version": "==24.1.0" }, "pypng": { "hashes": [ @@ -731,91 +746,70 @@ ], "version": "==0.20220715.0" }, - "pyrsistent": { - "hashes": [ - "sha256:016ad1afadf318eb7911baa24b049909f7f3bb2c5b1ed7b6a8f21db21ea3faa8", - "sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440", - "sha256:20460ac0ea439a3e79caa1dbd560344b64ed75e85d8703943e0b66c2a6150e4a", - "sha256:3311cb4237a341aa52ab8448c27e3a9931e2ee09561ad150ba94e4cfd3fc888c", - "sha256:3a8cb235fa6d3fd7aae6a4f1429bbb1fec1577d978098da1252f0489937786f3", - "sha256:3ab2204234c0ecd8b9368dbd6a53e83c3d4f3cab10ecaf6d0e772f456c442393", - "sha256:42ac0b2f44607eb92ae88609eda931a4f0dfa03038c44c772e07f43e738bcac9", - "sha256:49c32f216c17148695ca0e02a5c521e28a4ee6c5089f97e34fe24163113722da", - "sha256:4b774f9288dda8d425adb6544e5903f1fb6c273ab3128a355c6b972b7df39dcf", - "sha256:4c18264cb84b5e68e7085a43723f9e4c1fd1d935ab240ce02c0324a8e01ccb64", - "sha256:5a474fb80f5e0d6c9394d8db0fc19e90fa540b82ee52dba7d246a7791712f74a", - "sha256:64220c429e42a7150f4bfd280f6f4bb2850f95956bde93c6fda1b70507af6ef3", - "sha256:878433581fc23e906d947a6814336eee031a00e6defba224234169ae3d3d6a98", - "sha256:99abb85579e2165bd8522f0c0138864da97847875ecbd45f3e7e2af569bfc6f2", - "sha256:a2471f3f8693101975b1ff85ffd19bb7ca7dd7c38f8a81701f67d6b4f97b87d8", - "sha256:aeda827381f5e5d65cced3024126529ddc4289d944f75e090572c77ceb19adbf", - "sha256:b735e538f74ec31378f5a1e3886a26d2ca6351106b4dfde376a26fc32a044edc", - "sha256:c147257a92374fde8498491f53ffa8f4822cd70c0d85037e09028e478cababb7", - "sha256:c4db1bd596fefd66b296a3d5d943c94f4fac5bcd13e99bffe2ba6a759d959a28", - "sha256:c74bed51f9b41c48366a286395c67f4e894374306b197e62810e0fdaf2364da2", - "sha256:c9bb60a40a0ab9aba40a59f68214eed5a29c6274c83b2cc206a359c4a89fa41b", - "sha256:cc5d149f31706762c1f8bda2e8c4f8fead6e80312e3692619a75301d3dbb819a", - "sha256:ccf0d6bd208f8111179f0c26fdf84ed7c3891982f2edaeae7422575f47e66b64", - "sha256:e42296a09e83028b3476f7073fcb69ffebac0e66dbbfd1bd847d61f74db30f19", - "sha256:e8f2b814a3dc6225964fa03d8582c6e0b6650d68a232df41e3cc1b66a5d2f8d1", - "sha256:f0774bf48631f3a20471dd7c5989657b639fd2d285b861237ea9e82c36a415a9", - "sha256:f0e7c4b2f77593871e918be000b96c8107da48444d57005b6a6bc61fb4331b2c" - ], - "markers": "python_version >= '3.7'", - "version": "==0.19.3" - }, - "pytz": { + "python-ipware": { "hashes": [ - "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0", - "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a" + "sha256:9117b1c4dddcb5d5ca49e6a9617de2fc66aec2ef35394563ac4eecabdf58c062", + "sha256:fc936e6e7ec9fcc107f9315df40658f468ac72f739482a707181742882e36b60" ], - "version": "==2022.7.1" + "markers": "python_version >= '3.7'", + "version": "==3.0.0" }, "pyyaml": { "hashes": [ - "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", - "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", - "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", - "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", - "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", - "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", - "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", - "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", - "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", - "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", - "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", - "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", - "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782", - "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", - "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", - "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", - "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", - "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", - "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1", - "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", - "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", - "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", - "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", - "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", - "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", - "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d", - "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", - "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", - "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7", - "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", - "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", - "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", - "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358", - "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", - "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", - "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", - "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", - "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f", - "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", - "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" + "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", + "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", + "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", + "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", + "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", + "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", + "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", + "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", + "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", + "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", + "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", + "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", + "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", + "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", + "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", + "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", + "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", + "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", + "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", + "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", + "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", + "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", + "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", + "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", + "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", + "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", + "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", + "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", + "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", + "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", + "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", + "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", + "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", + "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", + "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", + "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", + "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", + "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", + "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", + "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", + "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", + "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", + "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", + "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", + "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", + "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", + "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", + "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", + "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", + "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", + "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" ], "markers": "python_version >= '3.6'", - "version": "==6.0" + "version": "==6.0.1" }, "qrcode": { "hashes": [ @@ -823,38 +817,154 @@ "sha256:9dd969454827e127dbd93696b20747239e6d540e082937c90f14ac95b30f5845" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==7.4.2" }, "redis": { "hashes": [ - "sha256:1eec3741cda408d3a5f84b78d089c8b8d895f21b3b050988351e925faf202864", - "sha256:5deb072d26e67d2be1712603bfb7947ec3431fb0eec9c578994052e33035af6d" + "sha256:7adc2835c7a9b5033b7ad8f8918d09b7344188228809c98df07af226d39dec91", + "sha256:ec31f2ed9675cc54c21ba854cfe0462e6faf1d83c8ce5944709db8a4700b9c61" ], "index": "pypi", - "version": "==4.5.1" + "markers": "python_version >= '3.7'", + "version": "==5.0.4" + }, + "referencing": { + "hashes": [ + "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c", + "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de" + ], + "markers": "python_version >= '3.8'", + "version": "==0.35.1" + }, + "rpds-py": { + "hashes": [ + "sha256:01e36a39af54a30f28b73096dd39b6802eddd04c90dbe161c1b8dbe22353189f", + "sha256:044a3e61a7c2dafacae99d1e722cc2d4c05280790ec5a05031b3876809d89a5c", + "sha256:08231ac30a842bd04daabc4d71fddd7e6d26189406d5a69535638e4dcb88fe76", + "sha256:08f9ad53c3f31dfb4baa00da22f1e862900f45908383c062c27628754af2e88e", + "sha256:0ab39c1ba9023914297dd88ec3b3b3c3f33671baeb6acf82ad7ce883f6e8e157", + "sha256:0af039631b6de0397ab2ba16eaf2872e9f8fca391b44d3d8cac317860a700a3f", + "sha256:0b8612cd233543a3781bc659c731b9d607de65890085098986dfd573fc2befe5", + "sha256:11a8c85ef4a07a7638180bf04fe189d12757c696eb41f310d2426895356dcf05", + "sha256:1374f4129f9bcca53a1bba0bb86bf78325a0374577cf7e9e4cd046b1e6f20e24", + "sha256:1d4acf42190d449d5e89654d5c1ed3a4f17925eec71f05e2a41414689cda02d1", + "sha256:1d9a5be316c15ffb2b3c405c4ff14448c36b4435be062a7f578ccd8b01f0c4d8", + "sha256:1df3659d26f539ac74fb3b0c481cdf9d725386e3552c6fa2974f4d33d78e544b", + "sha256:22806714311a69fd0af9b35b7be97c18a0fc2826e6827dbb3a8c94eac6cf7eeb", + "sha256:2644e47de560eb7bd55c20fc59f6daa04682655c58d08185a9b95c1970fa1e07", + "sha256:2e6d75ab12b0bbab7215e5d40f1e5b738aa539598db27ef83b2ec46747df90e1", + "sha256:30f43887bbae0d49113cbaab729a112251a940e9b274536613097ab8b4899cf6", + "sha256:34b18ba135c687f4dac449aa5157d36e2cbb7c03cbea4ddbd88604e076aa836e", + "sha256:36b3ee798c58ace201289024b52788161e1ea133e4ac93fba7d49da5fec0ef9e", + "sha256:39514da80f971362f9267c600b6d459bfbbc549cffc2cef8e47474fddc9b45b1", + "sha256:39f5441553f1c2aed4de4377178ad8ff8f9d733723d6c66d983d75341de265ab", + "sha256:3a96e0c6a41dcdba3a0a581bbf6c44bb863f27c541547fb4b9711fd8cf0ffad4", + "sha256:3f26b5bd1079acdb0c7a5645e350fe54d16b17bfc5e71f371c449383d3342e17", + "sha256:41ef53e7c58aa4ef281da975f62c258950f54b76ec8e45941e93a3d1d8580594", + "sha256:42821446ee7a76f5d9f71f9e33a4fb2ffd724bb3e7f93386150b61a43115788d", + "sha256:43fbac5f22e25bee1d482c97474f930a353542855f05c1161fd804c9dc74a09d", + "sha256:4457a94da0d5c53dc4b3e4de1158bdab077db23c53232f37a3cb7afdb053a4e3", + "sha256:465a3eb5659338cf2a9243e50ad9b2296fa15061736d6e26240e713522b6235c", + "sha256:482103aed1dfe2f3b71a58eff35ba105289b8d862551ea576bd15479aba01f66", + "sha256:4832d7d380477521a8c1644bbab6588dfedea5e30a7d967b5fb75977c45fd77f", + "sha256:4901165d170a5fde6f589acb90a6b33629ad1ec976d4529e769c6f3d885e3e80", + "sha256:5307def11a35f5ae4581a0b658b0af8178c65c530e94893345bebf41cc139d33", + "sha256:5417558f6887e9b6b65b4527232553c139b57ec42c64570569b155262ac0754f", + "sha256:56a737287efecafc16f6d067c2ea0117abadcd078d58721f967952db329a3e5c", + "sha256:586f8204935b9ec884500498ccc91aa869fc652c40c093bd9e1471fbcc25c022", + "sha256:5b4e7d8d6c9b2e8ee2d55c90b59c707ca59bc30058269b3db7b1f8df5763557e", + "sha256:5ddcba87675b6d509139d1b521e0c8250e967e63b5909a7e8f8944d0f90ff36f", + "sha256:618a3d6cae6ef8ec88bb76dd80b83cfe415ad4f1d942ca2a903bf6b6ff97a2da", + "sha256:635dc434ff724b178cb192c70016cc0ad25a275228f749ee0daf0eddbc8183b1", + "sha256:661d25cbffaf8cc42e971dd570d87cb29a665f49f4abe1f9e76be9a5182c4688", + "sha256:66e6a3af5a75363d2c9a48b07cb27c4ea542938b1a2e93b15a503cdfa8490795", + "sha256:67071a6171e92b6da534b8ae326505f7c18022c6f19072a81dcf40db2638767c", + "sha256:685537e07897f173abcf67258bee3c05c374fa6fff89d4c7e42fb391b0605e98", + "sha256:69e64831e22a6b377772e7fb337533c365085b31619005802a79242fee620bc1", + "sha256:6b0817e34942b2ca527b0e9298373e7cc75f429e8da2055607f4931fded23e20", + "sha256:6c81e5f372cd0dc5dc4809553d34f832f60a46034a5f187756d9b90586c2c307", + "sha256:6d7faa6f14017c0b1e69f5e2c357b998731ea75a442ab3841c0dbbbfe902d2c4", + "sha256:6ef0befbb5d79cf32d0266f5cff01545602344eda89480e1dd88aca964260b18", + "sha256:6ef687afab047554a2d366e112dd187b62d261d49eb79b77e386f94644363294", + "sha256:7223a2a5fe0d217e60a60cdae28d6949140dde9c3bcc714063c5b463065e3d66", + "sha256:77f195baa60a54ef9d2de16fbbfd3ff8b04edc0c0140a761b56c267ac11aa467", + "sha256:793968759cd0d96cac1e367afd70c235867831983f876a53389ad869b043c948", + "sha256:7bd339195d84439cbe5771546fe8a4e8a7a045417d8f9de9a368c434e42a721e", + "sha256:7cd863afe7336c62ec78d7d1349a2f34c007a3cc6c2369d667c65aeec412a5b1", + "sha256:7f2facbd386dd60cbbf1a794181e6aa0bd429bd78bfdf775436020172e2a23f0", + "sha256:84ffab12db93b5f6bad84c712c92060a2d321b35c3c9960b43d08d0f639d60d7", + "sha256:8c8370641f1a7f0e0669ddccca22f1da893cef7628396431eb445d46d893e5cd", + "sha256:8db715ebe3bb7d86d77ac1826f7d67ec11a70dbd2376b7cc214199360517b641", + "sha256:8e8916ae4c720529e18afa0b879473049e95949bf97042e938530e072fde061d", + "sha256:8f03bccbd8586e9dd37219bce4d4e0d3ab492e6b3b533e973fa08a112cb2ffc9", + "sha256:8f2fc11e8fe034ee3c34d316d0ad8808f45bc3b9ce5857ff29d513f3ff2923a1", + "sha256:923d39efa3cfb7279a0327e337a7958bff00cc447fd07a25cddb0a1cc9a6d2da", + "sha256:93df1de2f7f7239dc9cc5a4a12408ee1598725036bd2dedadc14d94525192fc3", + "sha256:998e33ad22dc7ec7e030b3df701c43630b5bc0d8fbc2267653577e3fec279afa", + "sha256:99f70b740dc04d09e6b2699b675874367885217a2e9f782bdf5395632ac663b7", + "sha256:9a00312dea9310d4cb7dbd7787e722d2e86a95c2db92fbd7d0155f97127bcb40", + "sha256:9d54553c1136b50fd12cc17e5b11ad07374c316df307e4cfd6441bea5fb68496", + "sha256:9dbbeb27f4e70bfd9eec1be5477517365afe05a9b2c441a0b21929ee61048124", + "sha256:a1ce3ba137ed54f83e56fb983a5859a27d43a40188ba798993812fed73c70836", + "sha256:a34d557a42aa28bd5c48a023c570219ba2593bcbbb8dc1b98d8cf5d529ab1434", + "sha256:a5f446dd5055667aabaee78487f2b5ab72e244f9bc0b2ffebfeec79051679984", + "sha256:ad36cfb355e24f1bd37cac88c112cd7730873f20fb0bdaf8ba59eedf8216079f", + "sha256:aec493917dd45e3c69d00a8874e7cbed844efd935595ef78a0f25f14312e33c6", + "sha256:b316144e85316da2723f9d8dc75bada12fa58489a527091fa1d5a612643d1a0e", + "sha256:b34ae4636dfc4e76a438ab826a0d1eed2589ca7d9a1b2d5bb546978ac6485461", + "sha256:b34b7aa8b261c1dbf7720b5d6f01f38243e9b9daf7e6b8bc1fd4657000062f2c", + "sha256:bc362ee4e314870a70f4ae88772d72d877246537d9f8cb8f7eacf10884862432", + "sha256:bed88b9a458e354014d662d47e7a5baafd7ff81c780fd91584a10d6ec842cb73", + "sha256:c0013fe6b46aa496a6749c77e00a3eb07952832ad6166bd481c74bda0dcb6d58", + "sha256:c0b5dcf9193625afd8ecc92312d6ed78781c46ecbf39af9ad4681fc9f464af88", + "sha256:c4325ff0442a12113a6379af66978c3fe562f846763287ef66bdc1d57925d337", + "sha256:c463ed05f9dfb9baebef68048aed8dcdc94411e4bf3d33a39ba97e271624f8f7", + "sha256:c8362467a0fdeccd47935f22c256bec5e6abe543bf0d66e3d3d57a8fb5731863", + "sha256:cd5bf1af8efe569654bbef5a3e0a56eca45f87cfcffab31dd8dde70da5982475", + "sha256:cf1ea2e34868f6fbf070e1af291c8180480310173de0b0c43fc38a02929fc0e3", + "sha256:d62dec4976954a23d7f91f2f4530852b0c7608116c257833922a896101336c51", + "sha256:d68c93e381010662ab873fea609bf6c0f428b6d0bb00f2c6939782e0818d37bf", + "sha256:d7c36232a90d4755b720fbd76739d8891732b18cf240a9c645d75f00639a9024", + "sha256:dd18772815d5f008fa03d2b9a681ae38d5ae9f0e599f7dda233c439fcaa00d40", + "sha256:ddc2f4dfd396c7bfa18e6ce371cba60e4cf9d2e5cdb71376aa2da264605b60b9", + "sha256:e003b002ec72c8d5a3e3da2989c7d6065b47d9eaa70cd8808b5384fbb970f4ec", + "sha256:e32a92116d4f2a80b629778280103d2a510a5b3f6314ceccd6e38006b5e92dcb", + "sha256:e4461d0f003a0aa9be2bdd1b798a041f177189c1a0f7619fe8c95ad08d9a45d7", + "sha256:e541ec6f2ec456934fd279a3120f856cd0aedd209fc3852eca563f81738f6861", + "sha256:e546e768d08ad55b20b11dbb78a745151acbd938f8f00d0cfbabe8b0199b9880", + "sha256:ea7d4a99f3b38c37eac212dbd6ec42b7a5ec51e2c74b5d3223e43c811609e65f", + "sha256:ed4eb745efbff0a8e9587d22a84be94a5eb7d2d99c02dacf7bd0911713ed14dd", + "sha256:f8a2f084546cc59ea99fda8e070be2fd140c3092dc11524a71aa8f0f3d5a55ca", + "sha256:fcb25daa9219b4cf3a0ab24b0eb9a5cc8949ed4dc72acb8fa16b7e1681aa3c58", + "sha256:fdea4952db2793c4ad0bdccd27c1d8fdd1423a92f04598bc39425bcc2b8ee46e" + ], + "markers": "python_version >= '3.8'", + "version": "==0.18.0" }, "sentry-sdk": { "hashes": [ - "sha256:633edefead34d976ff22e7edc367cdf57768e24bc714615ccae746d9d91795ae", - "sha256:a900845bd78c263d49695d48ce78a4bce1030bbd917e0b6cc021fc000c901113" + "sha256:b54c54a2160f509cf2757260d0cf3885b608c6192c2555a3857e3a4d0f84bdb3", + "sha256:c278e0f523f6f0ee69dc43ad26dcdb1202dffe5ac326ae31472e012d941bee21" ], "index": "pypi", - "version": "==1.16.0" + "markers": "python_version >= '3.6'", + "version": "==2.0.1" }, "service-identity": { "hashes": [ - "sha256:6e6c6086ca271dc11b033d17c3a8bea9f24ebff920c587da090afc9519419d34", - "sha256:f0b0caac3d40627c3c04d7a51b6e06721857a0e10a8775f2d1d7e72901b3a7db" + "sha256:6829c9d62fb832c2e1c435629b0a8c476e1929881f28bee4d20bc24161009221", + "sha256:a28caf8130c8a5c1c7a6f5293faaf239bbfb7751e4862436920ee6f2616f568a" ], - "version": "==21.1.0" + "version": "==24.1.0" }, "setuptools": { "hashes": [ - "sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330", - "sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251" + "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987", + "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32" ], - "markers": "python_version >= '3.7'", - "version": "==67.4.0" + "markers": "python_version >= '3.8'", + "version": "==69.5.1" }, "six": { "hashes": [ @@ -866,30 +976,30 @@ }, "sqlparse": { "hashes": [ - "sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34", - "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268" + "sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93", + "sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663" ], - "markers": "python_version >= '3.5'", - "version": "==0.4.3" + "markers": "python_version >= '3.8'", + "version": "==0.5.0" }, "traitlets": { "hashes": [ - "sha256:9e6ec080259b9a5940c797d58b613b5e31441c2257b87c2e795c5228ae80d2d8", - "sha256:f6cde21a9c68cf756af02035f72d5a723bf607e862e7be33ece505abf4a3bad9" + "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", + "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f" ], - "markers": "python_version >= '3.7'", - "version": "==5.9.0" + "markers": "python_version >= '3.8'", + "version": "==5.14.3" }, "twisted": { "extras": [ "tls" ], "hashes": [ - "sha256:32acbd40a94f5f46e7b42c109bfae2b302250945561783a8b7a059048f2d4d31", - "sha256:86c55f712cc5ab6f6d64e02503352464f0400f66d4f079096d744080afcccbd0" + "sha256:039f2e6a49ab5108abd94de187fa92377abe5985c7a72d68d0ad266ba19eae63", + "sha256:6b38b6ece7296b5e122c9eb17da2eeab3d98a198f50ca9efd00fb03e5b4fd4ae" ], - "markers": "python_full_version >= '3.7.1'", - "version": "==22.10.0" + "markers": "python_full_version >= '3.8.0'", + "version": "==24.3.0" }, "txaio": { "hashes": [ @@ -901,11 +1011,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb", - "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4" + "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", + "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" ], - "markers": "python_version >= '3.7'", - "version": "==4.5.0" + "markers": "python_version >= '3.8'", + "version": "==4.11.0" }, "uritemplate": { "hashes": [ @@ -917,18 +1027,18 @@ }, "urllib3": { "hashes": [ - "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72", - "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1" + "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", + "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" ], - "markers": "python_version >= '3.6'", - "version": "==1.26.14" + "markers": "python_version >= '3.8'", + "version": "==2.2.1" }, "wcwidth": { "hashes": [ - "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e", - "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0" + "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", + "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5" ], - "version": "==0.2.6" + "version": "==0.2.13" }, "webencodings": { "hashes": [ @@ -939,197 +1049,209 @@ }, "whitenoise": { "hashes": [ - "sha256:599dc6ca57e48929dfeffb2e8e187879bfe2aed0d49ca419577005b7f2cc930b", - "sha256:a02d6660ad161ff17e3042653c8e3f5ecbb2a2481a006bde125b9efb9a30113a" + "sha256:8998f7370973447fac1e8ef6e8ded2c5209a7b1f67c1012866dbcd09681c3251", + "sha256:b1f9db9bf67dc183484d760b99f4080185633136a273a03f6436034a41064146" ], "index": "pypi", - "version": "==6.4.0" - }, - "zope.interface": { - "hashes": [ - "sha256:008b0b65c05993bb08912f644d140530e775cf1c62a072bf9340c2249e613c32", - "sha256:0217a9615531c83aeedb12e126611b1b1a3175013bbafe57c702ce40000eb9a0", - "sha256:0fb497c6b088818e3395e302e426850f8236d8d9f4ef5b2836feae812a8f699c", - "sha256:17ebf6e0b1d07ed009738016abf0d0a0f80388e009d0ac6e0ead26fc162b3b9c", - "sha256:311196634bb9333aa06f00fc94f59d3a9fddd2305c2c425d86e406ddc6f2260d", - "sha256:3218ab1a7748327e08ef83cca63eea7cf20ea7e2ebcb2522072896e5e2fceedf", - "sha256:404d1e284eda9e233c90128697c71acffd55e183d70628aa0bbb0e7a3084ed8b", - "sha256:4087e253bd3bbbc3e615ecd0b6dd03c4e6a1e46d152d3be6d2ad08fbad742dcc", - "sha256:40f4065745e2c2fa0dff0e7ccd7c166a8ac9748974f960cd39f63d2c19f9231f", - "sha256:5334e2ef60d3d9439c08baedaf8b84dc9bb9522d0dacbc10572ef5609ef8db6d", - "sha256:604cdba8f1983d0ab78edc29aa71c8df0ada06fb147cea436dc37093a0100a4e", - "sha256:6373d7eb813a143cb7795d3e42bd8ed857c82a90571567e681e1b3841a390d16", - "sha256:655796a906fa3ca67273011c9805c1e1baa047781fca80feeb710328cdbed87f", - "sha256:65c3c06afee96c654e590e046c4a24559e65b0a87dbff256cd4bd6f77e1a33f9", - "sha256:696f3d5493eae7359887da55c2afa05acc3db5fc625c49529e84bd9992313296", - "sha256:6e972493cdfe4ad0411fd9abfab7d4d800a7317a93928217f1a5de2bb0f0d87a", - "sha256:7579960be23d1fddecb53898035a0d112ac858c3554018ce615cefc03024e46d", - "sha256:765d703096ca47aa5d93044bf701b00bbce4d903a95b41fff7c3796e747b1f1d", - "sha256:7e66f60b0067a10dd289b29dceabd3d0e6d68be1504fc9d0bc209cf07f56d189", - "sha256:8a2ffadefd0e7206adc86e492ccc60395f7edb5680adedf17a7ee4205c530df4", - "sha256:959697ef2757406bff71467a09d940ca364e724c534efbf3786e86eee8591452", - "sha256:9d783213fab61832dbb10d385a319cb0e45451088abd45f95b5bb88ed0acca1a", - "sha256:a16025df73d24795a0bde05504911d306307c24a64187752685ff6ea23897cb0", - "sha256:a2ad597c8c9e038a5912ac3cf166f82926feff2f6e0dabdab956768de0a258f5", - "sha256:bfee1f3ff62143819499e348f5b8a7f3aa0259f9aca5e0ddae7391d059dce671", - "sha256:d169ccd0756c15bbb2f1acc012f5aab279dffc334d733ca0d9362c5beaebe88e", - "sha256:d514c269d1f9f5cd05ddfed15298d6c418129f3f064765295659798349c43e6f", - "sha256:d692374b578360d36568dd05efb8a5a67ab6d1878c29c582e37ddba80e66c396", - "sha256:dbaeb9cf0ea0b3bc4b36fae54a016933d64c6d52a94810a63c00f440ecb37dd7", - "sha256:dc26c8d44472e035d59d6f1177eb712888447f5799743da9c398b0339ed90b1b", - "sha256:e1574980b48c8c74f83578d1e77e701f8439a5d93f36a5a0af31337467c08fcf", - "sha256:e74a578172525c20d7223eac5f8ad187f10940dac06e40113d62f14f3adb1e8f", - "sha256:e945de62917acbf853ab968d8916290548df18dd62c739d862f359ecd25842a6", - "sha256:f0980d44b8aded808bec5059018d64692f0127f10510eca71f2f0ace8fb11188", - "sha256:f98d4bd7bbb15ca701d19b93263cc5edfd480c3475d163f137385f49e5b3a3a7", - "sha256:fb68d212efd057596dee9e6582daded9f8ef776538afdf5feceb3059df2d2e7b" + "markers": "python_version >= '3.8'", + "version": "==6.6.0" + }, + "zope-interface": { + "hashes": [ + "sha256:014bb94fe6bf1786da1aa044eadf65bc6437bcb81c451592987e5be91e70a91e", + "sha256:01a0b3dd012f584afcf03ed814bce0fc40ed10e47396578621509ac031be98bf", + "sha256:10cde8dc6b2fd6a1d0b5ca4be820063e46ddba417ab82bcf55afe2227337b130", + "sha256:187f7900b63845dcdef1be320a523dbbdba94d89cae570edc2781eb55f8c2f86", + "sha256:1b0c4c90e5eefca2c3e045d9f9ed9f1e2cdbe70eb906bff6b247e17119ad89a1", + "sha256:22e8a218e8e2d87d4d9342aa973b7915297a08efbebea5b25900c73e78ed468e", + "sha256:26c9a37fb395a703e39b11b00b9e921c48f82b6e32cc5851ad5d0618cd8876b5", + "sha256:2bb78c12c1ad3a20c0d981a043d133299117b6854f2e14893b156979ed4e1d2c", + "sha256:2c3cfb272bcb83650e6695d49ae0d14dd06dc694789a3d929f23758557a23d92", + "sha256:2f32010ffb87759c6a3ad1c65ed4d2e38e51f6b430a1ca11cee901ec2b42e021", + "sha256:3c8731596198198746f7ce2a4487a0edcbc9ea5e5918f0ab23c4859bce56055c", + "sha256:40aa8c8e964d47d713b226c5baf5f13cdf3a3169c7a2653163b17ff2e2334d10", + "sha256:4137025731e824eee8d263b20682b28a0bdc0508de9c11d6c6be54163e5b7c83", + "sha256:46034be614d1f75f06e7dcfefba21d609b16b38c21fc912b01a99cb29e58febb", + "sha256:483e118b1e075f1819b3c6ace082b9d7d3a6a5eb14b2b375f1b80a0868117920", + "sha256:4d6b229f5e1a6375f206455cc0a63a8e502ed190fe7eb15e94a312dc69d40299", + "sha256:567d54c06306f9c5b6826190628d66753b9f2b0422f4c02d7c6d2b97ebf0a24e", + "sha256:5683aa8f2639016fd2b421df44301f10820e28a9b96382a6e438e5c6427253af", + "sha256:600101f43a7582d5b9504a7c629a1185a849ce65e60fca0f6968dfc4b76b6d39", + "sha256:62e32f02b3f26204d9c02c3539c802afc3eefb19d601a0987836ed126efb1f21", + "sha256:69dedb790530c7ca5345899a1b4cb837cc53ba669051ea51e8c18f82f9389061", + "sha256:72d5efecad16c619a97744a4f0b67ce1bcc88115aa82fcf1dc5be9bb403bcc0b", + "sha256:8d407e0fd8015f6d5dfad481309638e1968d70e6644e0753f229154667dd6cd5", + "sha256:a058e6cf8d68a5a19cb5449f42a404f0d6c2778b897e6ce8fadda9cea308b1b0", + "sha256:a1adc14a2a9d5e95f76df625a9b39f4709267a483962a572e3f3001ef90ea6e6", + "sha256:a56fe1261230093bfeedc1c1a6cd6f3ec568f9b07f031c9a09f46b201f793a85", + "sha256:ad4524289d8dbd6fb5aa17aedb18f5643e7d48358f42c007a5ee51a2afc2a7c5", + "sha256:afa0491a9f154cf8519a02026dc85a416192f4cb1efbbf32db4a173ba28b289a", + "sha256:bf34840e102d1d0b2d39b1465918d90b312b1119552cebb61a242c42079817b9", + "sha256:c40df4aea777be321b7e68facb901bc67317e94b65d9ab20fb96e0eb3c0b60a1", + "sha256:d0e7321557c702bd92dac3c66a2f22b963155fdb4600133b6b29597f62b71b12", + "sha256:d165d7774d558ea971cb867739fb334faf68fc4756a784e689e11efa3becd59e", + "sha256:e78a183a3c2f555c2ad6aaa1ab572d1c435ba42f1dc3a7e8c82982306a19b785", + "sha256:e8fa0fb05083a1a4216b4b881fdefa71c5d9a106e9b094cd4399af6b52873e91", + "sha256:f83d6b4b22262d9a826c3bd4b2fbfafe1d0000f085ef8e44cd1328eea274ae6a", + "sha256:f95bebd0afe86b2adc074df29edb6848fc4d474ff24075e2c263d698774e108d" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==5.5.2" + "markers": "python_version >= '3.7'", + "version": "==6.3" } }, "develop": { "alabaster": { "hashes": [ - "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3", - "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2" + "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", + "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92" ], - "markers": "python_version >= '3.6'", - "version": "==0.7.13" + "markers": "python_version >= '3.9'", + "version": "==0.7.16" + }, + "anyio": { + "hashes": [ + "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", + "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6" + ], + "markers": "python_version >= '3.8'", + "version": "==4.3.0" }, "asgiref": { "hashes": [ - "sha256:71e68008da809b957b7ee4b43dbccff33d1b23519fb8344e33f049897077afac", - "sha256:9567dfe7bd8d3c8c892227827c41cce860b368104c3431da67a0c5a65a949506" + "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", + "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590" ], "index": "pypi", - "version": "==3.6.0" + "markers": "python_version >= '3.8'", + "version": "==3.8.1" }, "babel": { "hashes": [ - "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610", - "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455" + "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363", + "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287" ], "markers": "python_version >= '3.7'", - "version": "==2.12.1" + "version": "==2.14.0" }, "certifi": { "hashes": [ - "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", - "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" + "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", + "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1" ], "markers": "python_version >= '3.6'", - "version": "==2022.12.7" + "version": "==2024.2.2" }, "cfgv": { "hashes": [ - "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426", - "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736" + "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", + "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560" ], - "markers": "python_full_version >= '3.6.1'", - "version": "==3.3.1" + "markers": "python_version >= '3.8'", + "version": "==3.4.0" }, "charset-normalizer": { "hashes": [ - "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b", - "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42", - "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d", - "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b", - "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a", - "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59", - "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154", - "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1", - "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c", - "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a", - "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d", - "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6", - "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b", - "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b", - "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783", - "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5", - "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918", - "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555", - "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639", - "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786", - "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e", - "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed", - "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820", - "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8", - "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3", - "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541", - "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14", - "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be", - "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e", - "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76", - "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b", - "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c", - "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b", - "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3", - "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc", - "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6", - "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59", - "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4", - "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d", - "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d", - "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3", - "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a", - "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea", - "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6", - "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e", - "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603", - "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24", - "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a", - "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58", - "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678", - "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a", - "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c", - "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6", - "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18", - "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174", - "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317", - "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f", - "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc", - "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837", - "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41", - "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c", - "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579", - "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753", - "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8", - "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291", - "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087", - "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866", - "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3", - "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d", - "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1", - "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca", - "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e", - "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db", - "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72", - "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d", - "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc", - "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539", - "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d", - "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af", - "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b", - "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602", - "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f", - "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478", - "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c", - "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e", - "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479", - "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7", - "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8" - ], - "markers": "python_version >= '3.6'", - "version": "==3.0.1" + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.3.2" }, "click": { "hashes": [ - "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", - "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48" + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" ], "markers": "python_version >= '3.7'", - "version": "==8.1.3" + "version": "==8.1.7" }, "colorama": { "hashes": [ @@ -1141,57 +1263,67 @@ }, "distlib": { "hashes": [ - "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46", - "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e" + "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784", + "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64" ], - "version": "==0.3.6" + "version": "==0.3.8" }, "django": { "hashes": [ - "sha256:44f714b81c5f190d9d2ddad01a532fe502fa01c4cb8faf1d081f4264ed15dcd8", - "sha256:f2f431e75adc40039ace496ad3b9f17227022e8b11566f4b363da44c7e44761e" + "sha256:4bd01a8c830bb77a8a3b0e7d8b25b887e536ad17a81ba2dce5476135c73312bd", + "sha256:916423499d75d62da7aa038d19aef23d23498d8df229775eb0a6309ee1013775" ], "index": "pypi", - "version": "==4.1.7" + "markers": "python_version >= '3.10'", + "version": "==5.0.4" }, "django-debug-toolbar": { "hashes": [ - "sha256:24ef1a7d44d25e60d7951e378454c6509bf536dce7e7d9d36e7c387db499bc27", - "sha256:879f8a4672d41621c06a4d322dcffa630fc4df056cada6e417ed01db0e5e0478" + "sha256:0b0dddee5ea29b9cb678593bc0d7a6d76b21d7799cb68e091a2148341a80f3c4", + "sha256:e09b7dcb8417b743234dfc57c95a7c1d1d87a88844abd13b4c5387f807b31bf6" ], "index": "pypi", - "version": "==3.8.1" + "markers": "python_version >= '3.8'", + "version": "==4.3.0" }, "docutils": { "hashes": [ - "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c", - "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06" + "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6", + "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.18.1" + "markers": "python_version >= '3.7'", + "version": "==0.20.1" }, "filelock": { "hashes": [ - "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de", - "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d" + "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f", + "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a" + ], + "markers": "python_version >= '3.8'", + "version": "==3.14.0" + }, + "h11": { + "hashes": [ + "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", + "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761" ], "markers": "python_version >= '3.7'", - "version": "==3.9.0" + "version": "==0.14.0" }, "identify": { "hashes": [ - "sha256:89e144fa560cc4cffb6ef2ab5e9fb18ed9f9b3cb054384bab4b95c12f6c309fe", - "sha256:93aac7ecf2f6abf879b8f29a8002d3c6de7086b8c28d88e1ad15045a15ab63f9" + "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa", + "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d" ], - "markers": "python_version >= '3.7'", - "version": "==2.5.18" + "markers": "python_version >= '3.8'", + "version": "==2.5.36" }, "idna": { "hashes": [ - "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", - "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", + "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" ], - "version": "==3.4" + "version": "==3.7" }, "imagesize": { "hashes": [ @@ -1201,178 +1333,185 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.4.1" }, - "importlib-metadata": { - "hashes": [ - "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad", - "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d" - ], - "markers": "python_version < '3.10'", - "version": "==6.0.0" - }, "jinja2": { "hashes": [ - "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", - "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" + "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa", + "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90" ], "markers": "python_version >= '3.7'", - "version": "==3.1.2" - }, - "livereload": { - "hashes": [ - "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869", - "sha256:ad4ac6f53b2d62bb6ce1a5e6e96f1f00976a32348afedcb4b6d68df2a1d346e4" - ], - "version": "==2.6.3" + "version": "==3.1.3" }, "markupsafe": { "hashes": [ - "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed", - "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc", - "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2", - "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460", - "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7", - "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0", - "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1", - "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa", - "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03", - "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323", - "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65", - "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013", - "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036", - "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f", - "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4", - "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419", - "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2", - "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619", - "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a", - "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a", - "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd", - "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7", - "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666", - "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65", - "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859", - "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625", - "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff", - "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156", - "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd", - "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba", - "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f", - "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1", - "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094", - "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a", - "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513", - "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed", - "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d", - "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3", - "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147", - "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c", - "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603", - "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601", - "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a", - "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1", - "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d", - "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3", - "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54", - "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2", - "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6", - "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58" + "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", + "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", + "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", + "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", + "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", + "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", + "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", + "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", + "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", + "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", + "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", + "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", + "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", + "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", + "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", + "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", + "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", + "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", + "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", + "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", + "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", + "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", + "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", + "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", + "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", + "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", + "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", + "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", + "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", + "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", + "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", + "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", + "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", + "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", + "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", + "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", + "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", + "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", + "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", + "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", + "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", + "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", + "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", + "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", + "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", + "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", + "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", + "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", + "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", + "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", + "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", + "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", + "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", + "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", + "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", + "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", + "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", + "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", + "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", + "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" ], "markers": "python_version >= '3.7'", - "version": "==2.1.2" + "version": "==2.1.5" }, "nodeenv": { "hashes": [ - "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e", - "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b" + "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2", + "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", - "version": "==1.7.0" + "version": "==1.8.0" }, "packaging": { "hashes": [ - "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2", - "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97" + "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" ], "markers": "python_version >= '3.7'", - "version": "==23.0" + "version": "==24.0" }, "platformdirs": { "hashes": [ - "sha256:13b08a53ed71021350c9e300d4ea8668438fb0046ab3937ac9a29913a1a1350a", - "sha256:accc3665857288317f32c7bebb5a8e482ba717b474f3fc1d18ca7f9214be0cef" + "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf", + "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1" ], - "markers": "python_version >= '3.7'", - "version": "==3.1.0" + "markers": "python_version >= '3.8'", + "version": "==4.2.1" }, "pre-commit": { "hashes": [ - "sha256:b80254e60668e1dd1f5c03a1c9e0413941d61f568a57d745add265945f65bfe8", - "sha256:d63e6537f9252d99f65755ae5b79c989b462d511ebbc481b561db6a297e1e865" + "sha256:5eae9e10c2b5ac51577c3452ec0a490455c45a0533f7960f993a0d01e59decab", + "sha256:e209d61b8acdcf742404408531f0c37d49d2c734fd7cff2d6076083d191cb060" ], "index": "pypi", - "version": "==3.1.1" + "markers": "python_version >= '3.9'", + "version": "==3.7.0" }, "pygments": { "hashes": [ - "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297", - "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717" + "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", + "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a" ], - "markers": "python_version >= '3.6'", - "version": "==2.14.0" + "markers": "python_version >= '3.8'", + "version": "==2.18.0" }, "pyyaml": { "hashes": [ - "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", - "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", - "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", - "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", - "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", - "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", - "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", - "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", - "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", - "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", - "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", - "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", - "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782", - "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", - "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", - "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", - "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", - "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", - "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1", - "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", - "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", - "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", - "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", - "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", - "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", - "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d", - "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", - "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", - "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7", - "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", - "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", - "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", - "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358", - "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", - "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", - "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", - "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", - "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f", - "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", - "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" + "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", + "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", + "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", + "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", + "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", + "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", + "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", + "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", + "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", + "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", + "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", + "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", + "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", + "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", + "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", + "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", + "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", + "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", + "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", + "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", + "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", + "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", + "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", + "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", + "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", + "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", + "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", + "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", + "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", + "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", + "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", + "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", + "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", + "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", + "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", + "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", + "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", + "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", + "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", + "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", + "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", + "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", + "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", + "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", + "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", + "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", + "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", + "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", + "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", + "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", + "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" ], "markers": "python_version >= '3.6'", - "version": "==6.0" + "version": "==6.0.1" }, "requests": { "hashes": [ - "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", - "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" + "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f", + "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], - "markers": "python_version >= '3.7' and python_version < '4'", - "version": "==2.28.2" + "markers": "python_version >= '3.7'", + "version": "==2.31.0" }, "selenium": { "hashes": [ @@ -1384,19 +1523,19 @@ }, "setuptools": { "hashes": [ - "sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330", - "sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251" + "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987", + "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32" ], - "markers": "python_version >= '3.7'", - "version": "==67.4.0" + "markers": "python_version >= '3.8'", + "version": "==69.5.1" }, - "six": { + "sniffio": { "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", + "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" + "markers": "python_version >= '3.7'", + "version": "==1.3.1" }, "snowballstemmer": { "hashes": [ @@ -1407,67 +1546,71 @@ }, "sphinx": { "hashes": [ - "sha256:0dac3b698538ffef41716cf97ba26c1c7788dba73ce6f150c1ff5b4720786dd2", - "sha256:807d1cb3d6be87eb78a381c3e70ebd8d346b9a25f3753e9947e866b2786865fc" + "sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3", + "sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc" ], "index": "pypi", - "version": "==6.1.3" + "markers": "python_version >= '3.9'", + "version": "==7.3.7" }, "sphinx-autobuild": { "hashes": [ - "sha256:8fe8cbfdb75db04475232f05187c776f46f6e9e04cacf1e49ce81bdac649ccac", - "sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05" + "sha256:1c0ed37a1970eed197f9c5a66d65759e7c4e4cba7b5a5d77940752bf1a59f2c7", + "sha256:f2522779d30fcbf0253e09714f274ce8c608cb6ebcd67922b1c54de59faba702" ], "index": "pypi", - "version": "==2021.3.14" + "markers": "python_version >= '3.9'", + "version": "==2024.4.16" }, "sphinx-intl": { "hashes": [ - "sha256:9798946b995989de691387651d70c3fc191275b587e2e519655541edfd7bbd68", - "sha256:9d9849ae42515b39786824e99f1e30db0404c377b01bb022690fc932b0221c02" + "sha256:56ad5f360fae4aa1cb963448c802f141b55c87223bb32a7b29e936620bd1a381", + "sha256:66976a85d31624dfcb564059a6918f90b31669269bfe3f30b2d72e81f225ab20" ], "index": "pypi", - "version": "==2.1.0" + "markers": "python_version >= '3.7'", + "version": "==2.2.0" }, "sphinx-rtd-theme": { "hashes": [ - "sha256:a0d8bd1a2ed52e0b338cbe19c4b2eef3c5e7a048769753dac6a9f059c7b641b8", - "sha256:f823f7e71890abe0ac6aaa6013361ea2696fc8d3e1fa798f463e82bdb77eeff2" + "sha256:bd5d7b80622406762073a04ef8fadc5f9151261563d47027de09910ce03afe6b", + "sha256:ec93d0856dc280cf3aee9a4c9807c60e027c7f7b461b77aeffed682e68f0e586" ], "index": "pypi", - "version": "==1.2.0" + "markers": "python_version >= '3.6'", + "version": "==2.0.0" }, "sphinxcontrib-applehelp": { "hashes": [ - "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228", - "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e" + "sha256:c40a4f96f3776c4393d933412053962fac2b84f4c99a7982ba42e09576a70619", + "sha256:cb61eb0ec1b61f349e5cc36b2028e9e7ca765be05e49641c97241274753067b4" ], - "markers": "python_version >= '3.8'", - "version": "==1.0.4" + "markers": "python_version >= '3.9'", + "version": "==1.0.8" }, "sphinxcontrib-devhelp": { "hashes": [ - "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", - "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" + "sha256:6485d09629944511c893fa11355bda18b742b83a2b181f9a009f7e500595c90f", + "sha256:9893fd3f90506bc4b97bdb977ceb8fbd823989f4316b28c3841ec128544372d3" ], - "markers": "python_version >= '3.5'", - "version": "==1.0.2" + "markers": "python_version >= '3.9'", + "version": "==1.0.6" }, "sphinxcontrib-htmlhelp": { "hashes": [ - "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff", - "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903" + "sha256:0dc87637d5de53dd5eec3a6a01753b1ccf99494bd756aafecd74b4fa9e729015", + "sha256:393f04f112b4d2f53d93448d4bce35842f62b307ccdc549ec1585e950bc35e04" ], - "markers": "python_version >= '3.8'", - "version": "==2.0.1" + "markers": "python_version >= '3.9'", + "version": "==2.0.5" }, "sphinxcontrib-jquery": { "hashes": [ - "sha256:8fb65f6dba84bf7bcd1aea1f02ab3955ac34611d838bcc95d4983b805b234daa", - "sha256:ed47fa425c338ffebe3c37e1cdb56e30eb806116b85f01055b158c7057fdb995" + "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", + "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae" ], - "markers": "python_version >= '3.1'", - "version": "==2.0.0" + "markers": "python_version >= '2.7'", + "version": "==4.1" }, "sphinxcontrib-jsmath": { "hashes": [ @@ -1479,68 +1622,218 @@ }, "sphinxcontrib-qthelp": { "hashes": [ - "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", - "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" + "sha256:053dedc38823a80a7209a80860b16b722e9e0209e32fea98c90e4e6624588ed6", + "sha256:e2ae3b5c492d58fcbd73281fbd27e34b8393ec34a073c792642cd8e529288182" ], - "markers": "python_version >= '3.5'", - "version": "==1.0.3" + "markers": "python_version >= '3.9'", + "version": "==1.0.7" }, "sphinxcontrib-serializinghtml": { "hashes": [ - "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd", - "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952" + "sha256:326369b8df80a7d2d8d7f99aa5ac577f51ea51556ed974e7716cfd4fca3f6cb7", + "sha256:93f3f5dc458b91b192fe10c397e324f262cf163d79f3282c158e8436a2c4511f" ], - "markers": "python_version >= '3.5'", - "version": "==1.1.5" + "markers": "python_version >= '3.9'", + "version": "==1.1.10" }, "sqlparse": { "hashes": [ - "sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34", - "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268" + "sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93", + "sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663" ], - "markers": "python_version >= '3.5'", - "version": "==0.4.3" + "markers": "python_version >= '3.8'", + "version": "==0.5.0" }, - "tornado": { + "starlette": { "hashes": [ - "sha256:1d54d13ab8414ed44de07efecb97d4ef7c39f7438cf5e976ccd356bebb1b5fca", - "sha256:20f638fd8cc85f3cbae3c732326e96addff0a15e22d80f049e00121651e82e72", - "sha256:5c87076709343557ef8032934ce5f637dbb552efa7b21d08e89ae7619ed0eb23", - "sha256:5f8c52d219d4995388119af7ccaa0bcec289535747620116a58d830e7c25d8a8", - "sha256:6fdfabffd8dfcb6cf887428849d30cf19a3ea34c2c248461e1f7d718ad30b66b", - "sha256:87dcafae3e884462f90c90ecc200defe5e580a7fbbb4365eda7c7c1eb809ebc9", - "sha256:9b630419bde84ec666bfd7ea0a4cb2a8a651c2d5cccdbdd1972a0c859dfc3c13", - "sha256:b8150f721c101abdef99073bf66d3903e292d851bee51910839831caba341a75", - "sha256:ba09ef14ca9893954244fd872798b4ccb2367c165946ce2dd7376aebdde8e3ac", - "sha256:d3a2f5999215a3a06a4fc218026cd84c61b8b2b40ac5296a6db1f1451ef04c1e", - "sha256:e5f923aa6a47e133d1cf87d60700889d7eae68988704e20c75fb2d65677a8e4b" + "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee", + "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823" ], - "markers": "python_version > '2.7'", - "version": "==6.2" + "markers": "python_version >= '3.8'", + "version": "==0.37.2" }, "urllib3": { "hashes": [ - "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72", - "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1" + "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", + "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" ], - "markers": "python_version >= '3.6'", - "version": "==1.26.14" + "markers": "python_version >= '3.8'", + "version": "==2.2.1" }, - "virtualenv": { + "uvicorn": { "hashes": [ - "sha256:3c22fa5a7c7aa106ced59934d2c20a2ecb7f49b4130b8bf444178a16b880fa45", - "sha256:a8a4b8ca1e28f864b7514a253f98c1d62b64e31e77325ba279248c65fb4fcef4" + "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de", + "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0" ], - "markers": "python_version >= '3.7'", - "version": "==20.20.0" + "markers": "python_version >= '3.8'", + "version": "==0.29.0" }, - "zipp": { + "virtualenv": { "hashes": [ - "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b", - "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556" + "sha256:604bfdceaeece392802e6ae48e69cec49168b9c5f4a44e483963f9242eb0e78b", + "sha256:7aa9982a728ae5892558bff6a2839c00b9ed145523ece2274fad6f414690ae75" ], "markers": "python_version >= '3.7'", - "version": "==3.15.0" + "version": "==20.26.1" + }, + "watchfiles": { + "hashes": [ + "sha256:02b73130687bc3f6bb79d8a170959042eb56eb3a42df3671c79b428cd73f17cc", + "sha256:02d91cbac553a3ad141db016e3350b03184deaafeba09b9d6439826ee594b365", + "sha256:06247538e8253975bdb328e7683f8515ff5ff041f43be6c40bff62d989b7d0b0", + "sha256:08dca260e85ffae975448e344834d765983237ad6dc308231aa16e7933db763e", + "sha256:0d9ac347653ebd95839a7c607608703b20bc07e577e870d824fa4801bc1cb124", + "sha256:0dd5fad9b9c0dd89904bbdea978ce89a2b692a7ee8a0ce19b940e538c88a809c", + "sha256:11cd0c3100e2233e9c53106265da31d574355c288e15259c0d40a4405cbae317", + "sha256:18722b50783b5e30a18a8a5db3006bab146d2b705c92eb9a94f78c72beb94094", + "sha256:18d5b4da8cf3e41895b34e8c37d13c9ed294954907929aacd95153508d5d89d7", + "sha256:1ad7247d79f9f55bb25ab1778fd47f32d70cf36053941f07de0b7c4e96b5d235", + "sha256:1b8d1eae0f65441963d805f766c7e9cd092f91e0c600c820c764a4ff71a0764c", + "sha256:1bd467213195e76f838caf2c28cd65e58302d0254e636e7c0fca81efa4a2e62c", + "sha256:1c9198c989f47898b2c22201756f73249de3748e0fc9de44adaf54a8b259cc0c", + "sha256:1fd9a5205139f3c6bb60d11f6072e0552f0a20b712c85f43d42342d162be1235", + "sha256:214cee7f9e09150d4fb42e24919a1e74d8c9b8a9306ed1474ecaddcd5479c293", + "sha256:27b4035013f1ea49c6c0b42d983133b136637a527e48c132d368eb19bf1ac6aa", + "sha256:3a23092a992e61c3a6a70f350a56db7197242f3490da9c87b500f389b2d01eef", + "sha256:3ad692bc7792be8c32918c699638b660c0de078a6cbe464c46e1340dadb94c19", + "sha256:3ccceb50c611c433145502735e0370877cced72a6c70fd2410238bcbc7fe51d8", + "sha256:3d0f32ebfaa9c6011f8454994f86108c2eb9c79b8b7de00b36d558cadcedaa3d", + "sha256:3f92944efc564867bbf841c823c8b71bb0be75e06b8ce45c084b46411475a915", + "sha256:40bca549fdc929b470dd1dbfcb47b3295cb46a6d2c90e50588b0a1b3bd98f429", + "sha256:43babacef21c519bc6631c5fce2a61eccdfc011b4bcb9047255e9620732c8097", + "sha256:4566006aa44cb0d21b8ab53baf4b9c667a0ed23efe4aaad8c227bfba0bf15cbe", + "sha256:49f56e6ecc2503e7dbe233fa328b2be1a7797d31548e7a193237dcdf1ad0eee0", + "sha256:4c48a10d17571d1275701e14a601e36959ffada3add8cdbc9e5061a6e3579a5d", + "sha256:4ea10a29aa5de67de02256a28d1bf53d21322295cb00bd2d57fcd19b850ebd99", + "sha256:511f0b034120cd1989932bf1e9081aa9fb00f1f949fbd2d9cab6264916ae89b1", + "sha256:51ddac60b96a42c15d24fbdc7a4bfcd02b5a29c047b7f8bf63d3f6f5a860949a", + "sha256:57d430f5fb63fea141ab71ca9c064e80de3a20b427ca2febcbfcef70ff0ce895", + "sha256:59137c0c6826bd56c710d1d2bda81553b5e6b7c84d5a676747d80caf0409ad94", + "sha256:5a03651352fc20975ee2a707cd2d74a386cd303cc688f407296064ad1e6d1562", + "sha256:5eb86c6acb498208e7663ca22dbe68ca2cf42ab5bf1c776670a50919a56e64ab", + "sha256:642d66b75eda909fd1112d35c53816d59789a4b38c141a96d62f50a3ef9b3360", + "sha256:6674b00b9756b0af620aa2a3346b01f8e2a3dc729d25617e1b89cf6af4a54eb1", + "sha256:668c265d90de8ae914f860d3eeb164534ba2e836811f91fecc7050416ee70aa7", + "sha256:66fac0c238ab9a2e72d026b5fb91cb902c146202bbd29a9a1a44e8db7b710b6f", + "sha256:6c107ea3cf2bd07199d66f156e3ea756d1b84dfd43b542b2d870b77868c98c03", + "sha256:6c889025f59884423428c261f212e04d438de865beda0b1e1babab85ef4c0f01", + "sha256:6cb8fdc044909e2078c248986f2fc76f911f72b51ea4a4fbbf472e01d14faa58", + "sha256:6e9be3ef84e2bb9710f3f777accce25556f4a71e15d2b73223788d528fcc2052", + "sha256:7f762a1a85a12cc3484f77eee7be87b10f8c50b0b787bb02f4e357403cad0c0e", + "sha256:83a696da8922314ff2aec02987eefb03784f473281d740bf9170181829133765", + "sha256:853853cbf7bf9408b404754b92512ebe3e3a83587503d766d23e6bf83d092ee6", + "sha256:8ad3fe0a3567c2f0f629d800409cd528cb6251da12e81a1f765e5c5345fd0137", + "sha256:8c6ed10c2497e5fedadf61e465b3ca12a19f96004c15dcffe4bd442ebadc2d85", + "sha256:8d5f400326840934e3507701f9f7269247f7c026d1b6cfd49477d2be0933cfca", + "sha256:927c589500f9f41e370b0125c12ac9e7d3a2fd166b89e9ee2828b3dda20bfe6f", + "sha256:9a0aa47f94ea9a0b39dd30850b0adf2e1cd32a8b4f9c7aa443d852aacf9ca214", + "sha256:9b37a7ba223b2f26122c148bb8d09a9ff312afca998c48c725ff5a0a632145f7", + "sha256:9c873345680c1b87f1e09e0eaf8cf6c891b9851d8b4d3645e7efe2ec20a20cc7", + "sha256:9d09869f2c5a6f2d9df50ce3064b3391d3ecb6dced708ad64467b9e4f2c9bef3", + "sha256:9d353c4cfda586db2a176ce42c88f2fc31ec25e50212650c89fdd0f560ee507b", + "sha256:a1e3014a625bcf107fbf38eece0e47fa0190e52e45dc6eee5a8265ddc6dc5ea7", + "sha256:a3b9bec9579a15fb3ca2d9878deae789df72f2b0fdaf90ad49ee389cad5edab6", + "sha256:ab03a90b305d2588e8352168e8c5a1520b721d2d367f31e9332c4235b30b8994", + "sha256:aff06b2cac3ef4616e26ba17a9c250c1fe9dd8a5d907d0193f84c499b1b6e6a9", + "sha256:b3cab0e06143768499384a8a5efb9c4dc53e19382952859e4802f294214f36ec", + "sha256:b4a21f71885aa2744719459951819e7bf5a906a6448a6b2bbce8e9cc9f2c8128", + "sha256:b6d45d9b699ecbac6c7bd8e0a2609767491540403610962968d258fd6405c17c", + "sha256:be6dd5d52b73018b21adc1c5d28ac0c68184a64769052dfeb0c5d9998e7f56a2", + "sha256:c550a56bf209a3d987d5a975cdf2063b3389a5d16caf29db4bdddeae49f22078", + "sha256:c76c635fabf542bb78524905718c39f736a98e5ab25b23ec6d4abede1a85a6a3", + "sha256:c81818595eff6e92535ff32825f31c116f867f64ff8cdf6562cd1d6b2e1e8f3e", + "sha256:cfb92d49dbb95ec7a07511bc9efb0faff8fe24ef3805662b8d6808ba8409a71a", + "sha256:d23bcd6c8eaa6324fe109d8cac01b41fe9a54b8c498af9ce464c1aeeb99903d6", + "sha256:d5b1dc0e708fad9f92c296ab2f948af403bf201db8fb2eb4c8179db143732e49", + "sha256:d78f30cbe8b2ce770160d3c08cff01b2ae9306fe66ce899b73f0409dc1846c1b", + "sha256:d8f57c4461cd24fda22493109c45b3980863c58a25b8bec885ca8bea6b8d4b28", + "sha256:d9792dff410f266051025ecfaa927078b94cc7478954b06796a9756ccc7e14a9", + "sha256:e7941bbcfdded9c26b0bf720cb7e6fd803d95a55d2c14b4bd1f6a2772230c586", + "sha256:ebe684d7d26239e23d102a2bad2a358dedf18e462e8808778703427d1f584400", + "sha256:ec8c8900dc5c83650a63dd48c4d1d245343f904c4b64b48798c67a3767d7e165", + "sha256:f564bf68404144ea6b87a78a3f910cc8de216c6b12a4cf0b27718bf4ec38d303", + "sha256:fd7ac678b92b29ba630d8c842d8ad6c555abda1b9ef044d6cc092dacbfc9719d" + ], + "markers": "python_version >= '3.8'", + "version": "==0.21.0" + }, + "websockets": { + "hashes": [ + "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b", + "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6", + "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df", + "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b", + "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205", + "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892", + "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53", + "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2", + "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed", + "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c", + "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd", + "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b", + "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931", + "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30", + "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370", + "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be", + "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec", + "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf", + "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62", + "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b", + "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402", + "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f", + "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123", + "sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9", + "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603", + "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45", + "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558", + "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4", + "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438", + "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137", + "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480", + "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447", + "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8", + "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04", + "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c", + "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb", + "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967", + "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b", + "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d", + "sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def", + "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c", + "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92", + "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2", + "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113", + "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b", + "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28", + "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7", + "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d", + "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f", + "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468", + "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8", + "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae", + "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611", + "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d", + "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9", + "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca", + "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f", + "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2", + "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077", + "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2", + "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6", + "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374", + "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc", + "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e", + "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53", + "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399", + "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547", + "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3", + "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870", + "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5", + "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8", + "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7" + ], + "markers": "python_version >= '3.8'", + "version": "==12.0" } } } diff --git a/render.yaml b/render.yaml index 53b2fbbd92c..b32157cd14c 100644 --- a/render.yaml +++ b/render.yaml @@ -30,7 +30,7 @@ services: - key: DJANGO_SECRET_KEY generateValue: true - key: PYTHON_VERSION - value: "3.9.9" + value: "3.12.3" - key: WEB_CONCURRENCY value: 4 - key: TAB_DIRECTOR_EMAIL diff --git a/runtime.txt b/runtime.txt index 5402961979a..546f3c8de17 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.9.12 +python-3.11.9 diff --git a/tabbycat/adjallocation/migrations/0010_alter_adjudicatoradjudicatorconflict_unique_together_and_more.py b/tabbycat/adjallocation/migrations/0010_alter_adjudicatoradjudicatorconflict_unique_together_and_more.py new file mode 100644 index 00000000000..a297ce889b4 --- /dev/null +++ b/tabbycat/adjallocation/migrations/0010_alter_adjudicatoradjudicatorconflict_unique_together_and_more.py @@ -0,0 +1,56 @@ +# Generated by Django 5.0.4 on 2024-05-04 13:21 + +import utils.models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('adjallocation', '0009_auto_20200902_1208'), + ('draw', '0008_alter_debateteam_side_alter_teamsideallocation_side'), + ('participants', '0021_team_seed'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='adjudicatoradjudicatorconflict', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='adjudicatorinstitutionconflict', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='adjudicatorteamconflict', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='debateadjudicator', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='teaminstitutionconflict', + unique_together=set(), + ), + migrations.AddConstraint( + model_name='adjudicatoradjudicatorconflict', + constraint=utils.models.UniqueConstraint(fields=('adjudicator1', 'adjudicator2'), name='adjallo_adjudicatoradjudicatorconflict_adjudicator1__adjudicator2_uniq'), + ), + migrations.AddConstraint( + model_name='adjudicatorinstitutionconflict', + constraint=utils.models.UniqueConstraint(fields=('adjudicator', 'institution'), name='adjallo_adjudicatorinstitutionconflict_adjudicator__institution_uniq'), + ), + migrations.AddConstraint( + model_name='adjudicatorteamconflict', + constraint=utils.models.UniqueConstraint(fields=('adjudicator', 'team'), name='adjallo_adjudicatorteamconflict_adjudicator__team_uniq'), + ), + migrations.AddConstraint( + model_name='debateadjudicator', + constraint=utils.models.UniqueConstraint(fields=('debate', 'adjudicator'), name='adjallo_debateadjudicator_debate__adjudicator_uniq'), + ), + migrations.AddConstraint( + model_name='teaminstitutionconflict', + constraint=utils.models.UniqueConstraint(fields=('team', 'institution'), name='adjallo_teaminstitutionconflict_team__institution_uniq'), + ), + ] diff --git a/tabbycat/adjallocation/models.py b/tabbycat/adjallocation/models.py index 635503cc526..7dd0e65be68 100644 --- a/tabbycat/adjallocation/models.py +++ b/tabbycat/adjallocation/models.py @@ -1,6 +1,8 @@ from django.db import models from django.utils.translation import gettext_lazy as _ +from utils.models import UniqueConstraint + class DebateAdjudicatorManager(models.Manager): use_for_related_fields = True @@ -31,9 +33,9 @@ class DebateAdjudicator(models.Model): timing_confirmed = models.BooleanField(null=True, verbose_name=_("available?")) class Meta: + constraints = [UniqueConstraint(fields=['debate', 'adjudicator'])] verbose_name = _("debate adjudicator") verbose_name_plural = _("debate adjudicators") - unique_together = ('debate', 'adjudicator') def __str__(self): return '{} in {} ({})'.format(self.adjudicator, self.debate, self.get_type_display()) @@ -50,9 +52,9 @@ class AdjudicatorTeamConflict(models.Model): verbose_name=_("team")) class Meta: + constraints = [UniqueConstraint(fields=['adjudicator', 'team'])] verbose_name = _("adjudicator-team conflict") verbose_name_plural = _("adjudicator-team conflicts") - unique_together = ('adjudicator', 'team') def __str__(self): return '{} with {}'.format(self.adjudicator, self.team) @@ -67,9 +69,9 @@ class AdjudicatorAdjudicatorConflict(models.Model): verbose_name=_("adjudicator 2")) class Meta: + constraints = [UniqueConstraint(fields=['adjudicator1', 'adjudicator2'])] verbose_name = _("adjudicator-adjudicator conflict") verbose_name_plural = _("adjudicator-adjudicator conflicts") - unique_together = ('adjudicator1', 'adjudicator2') def __str__(self): return '{} with {}'.format(self.adjudicator1, self.adjudicator2) @@ -82,9 +84,9 @@ class AdjudicatorInstitutionConflict(models.Model): verbose_name=_("institution")) class Meta: + constraints = [UniqueConstraint(fields=['adjudicator', 'institution'])] verbose_name = _("adjudicator-institution conflict") verbose_name_plural = _("adjudicator-institution conflicts") - unique_together = ('adjudicator', 'institution') def __str__(self): return '{} with {}'.format(self.adjudicator, self.institution) @@ -97,9 +99,9 @@ class TeamInstitutionConflict(models.Model): verbose_name=_("institution")) class Meta: + constraints = [UniqueConstraint(fields=['team', 'institution'])] verbose_name = _("team-institution conflict") verbose_name_plural = _("team-institution conflicts") - unique_together = ('team', 'institution') def __str__(self): return '{} with {}'.format(self.team, self.institution) diff --git a/tabbycat/adjfeedback/migrations/0015_alter_adjudicatorfeedback_unique_together_and_more.py b/tabbycat/adjfeedback/migrations/0015_alter_adjudicatorfeedback_unique_together_and_more.py new file mode 100644 index 00000000000..3cfab4bcf8b --- /dev/null +++ b/tabbycat/adjfeedback/migrations/0015_alter_adjudicatorfeedback_unique_together_and_more.py @@ -0,0 +1,80 @@ +# Generated by Django 5.0.4 on 2024-05-04 13:21 + +import utils.models +from django.conf import settings +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('adjallocation', '0010_alter_adjudicatoradjudicatorconflict_unique_together_and_more'), + ('adjfeedback', '0014_alter_adjudicatorfeedback_submitter_type'), + ('draw', '0008_alter_debateteam_side_alter_teamsideallocation_side'), + ('participants', '0021_team_seed'), + ('tournaments', '0010_alter_round_draw_type'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='adjudicatorfeedback', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='adjudicatorfeedbackbooleananswer', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='adjudicatorfeedbackfloatanswer', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='adjudicatorfeedbackintegeranswer', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='adjudicatorfeedbackmanyanswer', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='adjudicatorfeedbackquestion', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='adjudicatorfeedbackstringanswer', + unique_together=set(), + ), + migrations.AddConstraint( + model_name='adjudicatorfeedback', + constraint=utils.models.UniqueConstraint(fields=('adjudicator', 'source_adjudicator', 'source_team', 'version'), name='adjfeed_adjudicatorfeedback_adjudicator__source_adjudicator__source_team__version_uniq'), + ), + migrations.AddConstraint( + model_name='adjudicatorfeedbackbooleananswer', + constraint=utils.models.UniqueConstraint(fields=('question', 'feedback'), name='adjfeed_adjudicatorfeedbackbooleananswer_question__feedback_uniq'), + ), + migrations.AddConstraint( + model_name='adjudicatorfeedbackfloatanswer', + constraint=utils.models.UniqueConstraint(fields=('question', 'feedback'), name='adjfeed_adjudicatorfeedbackfloatanswer_question__feedback_uniq'), + ), + migrations.AddConstraint( + model_name='adjudicatorfeedbackintegeranswer', + constraint=utils.models.UniqueConstraint(fields=('question', 'feedback'), name='adjfeed_adjudicatorfeedbackintegeranswer_question__feedback_uniq'), + ), + migrations.AddConstraint( + model_name='adjudicatorfeedbackmanyanswer', + constraint=utils.models.UniqueConstraint(fields=('question', 'feedback'), name='adjfeed_adjudicatorfeedbackmanyanswer_question__feedback_uniq'), + ), + migrations.AddConstraint( + model_name='adjudicatorfeedbackquestion', + constraint=utils.models.UniqueConstraint(fields=('tournament', 'reference'), name='adjfeed_adjudicatorfeedbackquestion_tournament__reference_uniq'), + ), + migrations.AddConstraint( + model_name='adjudicatorfeedbackquestion', + constraint=utils.models.UniqueConstraint(fields=('tournament', 'seq'), name='adjfeed_adjudicatorfeedbackquestion_tournament__seq_uniq'), + ), + migrations.AddConstraint( + model_name='adjudicatorfeedbackstringanswer', + constraint=utils.models.UniqueConstraint(fields=('question', 'feedback'), name='adjfeed_adjudicatorfeedbackstringanswer_question__feedback_uniq'), + ), + ] diff --git a/tabbycat/adjfeedback/models.py b/tabbycat/adjfeedback/models.py index b6524f7473b..d8613d073c7 100644 --- a/tabbycat/adjfeedback/models.py +++ b/tabbycat/adjfeedback/models.py @@ -7,6 +7,7 @@ from adjallocation.models import DebateAdjudicator from results.models import Submission +from utils.models import UniqueConstraint class AdjudicatorBaseScoreHistory(models.Model): @@ -34,7 +35,7 @@ class AdjudicatorFeedbackAnswer(models.Model): class Meta: abstract = True - unique_together = [('question', 'feedback')] + constraints = [UniqueConstraint(fields=['question', 'feedback'])] class AdjudicatorFeedbackBooleanAnswer(AdjudicatorFeedbackAnswer): @@ -179,7 +180,10 @@ class AdjudicatorFeedbackQuestion(models.Model): default=list) class Meta: - unique_together = [('tournament', 'reference'), ('tournament', 'seq')] + constraints = [ + UniqueConstraint(fields=['tournament', 'reference']), + UniqueConstraint(fields=['tournament', 'seq']), + ] verbose_name = _("adjudicator feedback question") verbose_name_plural = _("adjudicator feedback questions") @@ -242,7 +246,9 @@ class AdjudicatorFeedback(Submission): help_text=_("Whether the feedback should affect the adjudicator's score")) class Meta: - unique_together = [('adjudicator', 'source_adjudicator', 'source_team', 'version')] + constraints = [ + UniqueConstraint(fields=['adjudicator', 'source_adjudicator', 'source_team', 'version']), + ] verbose_name = _("adjudicator feedback") verbose_name_plural = _("adjudicator feedbacks") diff --git a/tabbycat/api/serializers.py b/tabbycat/api/serializers.py index 02a236a4b0f..ac5a3e8f139 100644 --- a/tabbycat/api/serializers.py +++ b/tabbycat/api/serializers.py @@ -750,7 +750,7 @@ def update(self, instance, validated_data): class InstitutionSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='api-global-institution-detail') - region = fields.CreatableSlugRelatedField(slug_field='name', queryset=Region.objects.all(), required=False) + region = fields.CreatableSlugRelatedField(slug_field='name', queryset=Region.objects.all(), required=False, allow_null=True) venue_constraints = VenueConstraintSerializer(many=True, required=False) class Meta: diff --git a/tabbycat/availability/migrations/0006_alter_roundavailability_unique_together_and_more.py b/tabbycat/availability/migrations/0006_alter_roundavailability_unique_together_and_more.py new file mode 100644 index 00000000000..3a2af28baf9 --- /dev/null +++ b/tabbycat/availability/migrations/0006_alter_roundavailability_unique_together_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 5.0.4 on 2024-05-04 13:21 + +import utils.models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('availability', '0005_auto_20180307_2217'), + ('contenttypes', '0002_remove_content_type_name'), + ('tournaments', '0010_alter_round_draw_type'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='roundavailability', + unique_together=set(), + ), + migrations.AddConstraint( + model_name='roundavailability', + constraint=utils.models.UniqueConstraint(fields=('round', 'content_type', 'object_id'), name='availab_roundavailability_round__content_type__object_id_uniq'), + ), + ] diff --git a/tabbycat/availability/models.py b/tabbycat/availability/models.py index d974d8bf43a..41bec62d974 100644 --- a/tabbycat/availability/models.py +++ b/tabbycat/availability/models.py @@ -3,6 +3,8 @@ from django.db import models from django.utils.translation import gettext_lazy as _ +from utils.models import UniqueConstraint + class RoundAvailability(models.Model): @@ -20,7 +22,9 @@ class RoundAvailability(models.Model): verbose_name=_("round")) class Meta: - unique_together = [('round', 'content_type', 'object_id')] + constraints = [ + UniqueConstraint(fields=['round', 'content_type', 'object_id']), + ] verbose_name = _("round availability") verbose_name_plural = _("round availabilities") diff --git a/tabbycat/breakqual/migrations/0006_alter_breakcategory_unique_together_and_more.py b/tabbycat/breakqual/migrations/0006_alter_breakcategory_unique_together_and_more.py new file mode 100644 index 00000000000..240b55757dd --- /dev/null +++ b/tabbycat/breakqual/migrations/0006_alter_breakcategory_unique_together_and_more.py @@ -0,0 +1,45 @@ +# Generated by Django 5.0.4 on 2024-05-04 13:21 + +import utils.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('breakqual', '0005_remove_league_fields'), + ('participants', '0021_team_seed'), + ('tournaments', '0010_alter_round_draw_type'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='breakcategory', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='breakingteam', + unique_together=set(), + ), + migrations.AlterIndexTogether( + name='breakcategory', + index_together=set(), + ), + migrations.AlterField( + model_name='breakcategory', + name='is_general', + field=models.BooleanField(help_text='Are teams eligible for this break by default', verbose_name='is general'), + ), + migrations.AddConstraint( + model_name='breakcategory', + constraint=utils.models.UniqueConstraint(fields=('tournament', 'seq'), name='breakqu_breakcategory_tournament__seq_uniq'), + ), + migrations.AddConstraint( + model_name='breakcategory', + constraint=utils.models.UniqueConstraint(fields=('tournament', 'slug'), name='breakqu_breakcategory_tournament__slug_uniq'), + ), + migrations.AddConstraint( + model_name='breakingteam', + constraint=utils.models.UniqueConstraint(fields=('break_category', 'team'), name='breakqu_breakingteam_break_category__team_uniq'), + ), + ] diff --git a/tabbycat/breakqual/models.py b/tabbycat/breakqual/models.py index e9628129976..da0ff377bb8 100644 --- a/tabbycat/breakqual/models.py +++ b/tabbycat/breakqual/models.py @@ -4,6 +4,8 @@ from django.db import models from django.utils.translation import gettext_lazy as _ +from utils.models import UniqueConstraint + class BreakCategory(models.Model): tournament = models.ForeignKey('tournaments.Tournament', models.CASCADE, @@ -51,9 +53,11 @@ def __str__(self): return "[{}] {}".format(self.tournament.slug, self.name) class Meta: - unique_together = [('tournament', 'seq'), ('tournament', 'slug')] + constraints = [ + UniqueConstraint(fields=['tournament', 'seq']), + UniqueConstraint(fields=['tournament', 'slug']), + ] ordering = ['tournament', 'seq'] - index_together = ['tournament', 'seq'] verbose_name = _("break category") verbose_name_plural = _("break categories") @@ -100,6 +104,6 @@ class BreakingTeam(models.Model): help_text=_("Used to explain why an otherwise-qualified team didn't break")) class Meta: - unique_together = [('break_category', 'team')] + constraints = [UniqueConstraint(fields=['break_category', 'team'])] verbose_name = _("breaking team") verbose_name_plural = _("breaking teams") diff --git a/tabbycat/draw/migrations/0009_alter_teamsideallocation_unique_together_and_more.py b/tabbycat/draw/migrations/0009_alter_teamsideallocation_unique_together_and_more.py new file mode 100644 index 00000000000..5282a33abc1 --- /dev/null +++ b/tabbycat/draw/migrations/0009_alter_teamsideallocation_unique_together_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 5.0.4 on 2024-05-04 13:21 + +import utils.models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('draw', '0008_alter_debateteam_side_alter_teamsideallocation_side'), + ('participants', '0022_rename_team_tournament_institution_short_reference_participant_tournam_160efa_idx_and_more'), + ('tournaments', '0011_alter_round_unique_together_and_more'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='teamsideallocation', + unique_together=set(), + ), + migrations.AddConstraint( + model_name='teamsideallocation', + constraint=utils.models.UniqueConstraint(fields=('round', 'team'), name='draw_teamsideallocation_round__team_uniq'), + ), + ] diff --git a/tabbycat/draw/models.py b/tabbycat/draw/models.py index 7b40c420413..a6ce5d14bd7 100644 --- a/tabbycat/draw/models.py +++ b/tabbycat/draw/models.py @@ -7,6 +7,7 @@ from tournaments.utils import get_side_name from utils.fields import ChoiceArrayField +from utils.models import UniqueConstraint from .generator import DRAW_FLAG_DESCRIPTIONS @@ -388,6 +389,6 @@ class TeamSideAllocation(models.Model): verbose_name=_("side")) class Meta: - unique_together = [('round', 'team')] + constraints = [UniqueConstraint(fields=['round', 'team'])] verbose_name = _("team side allocation") verbose_name_plural = _("team side allocations") diff --git a/tabbycat/motions/migrations/0006_alter_debateteammotionpreference_unique_together_and_more.py b/tabbycat/motions/migrations/0006_alter_debateteammotionpreference_unique_together_and_more.py new file mode 100644 index 00000000000..81725f63bb6 --- /dev/null +++ b/tabbycat/motions/migrations/0006_alter_debateteammotionpreference_unique_together_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.0.4 on 2024-05-04 13:21 + +import utils.models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('draw', '0009_alter_teamsideallocation_unique_together_and_more'), + ('motions', '0005_motions_mtm'), + ('results', '0015_alter_ballotsubmission_submitter_type'), + ('tournaments', '0011_alter_round_unique_together_and_more'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='debateteammotionpreference', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='roundmotion', + unique_together=set(), + ), + migrations.AddConstraint( + model_name='debateteammotionpreference', + constraint=utils.models.UniqueConstraint(fields=('debate_team', 'preference', 'ballot_submission'), name='motions_debateteammotionpreference_debate_team__preference__ballot_submission_uniq'), + ), + migrations.AddConstraint( + model_name='roundmotion', + constraint=utils.models.UniqueConstraint(fields=('round', 'seq'), name='motions_roundmotion_round__seq_uniq'), + ), + ] diff --git a/tabbycat/motions/models.py b/tabbycat/motions/models.py index f72be12ad5c..c1abc5a3198 100644 --- a/tabbycat/motions/models.py +++ b/tabbycat/motions/models.py @@ -2,6 +2,8 @@ from django.utils.translation import gettext_lazy as _ from html2text import html2text +from utils.models import UniqueConstraint + class Motion(models.Model): """Represents a single motion (not a set of motions).""" @@ -53,7 +55,7 @@ class DebateTeamMotionPreference(models.Model): verbose_name=_("ballot submission")) class Meta: - unique_together = [('debate_team', 'preference', 'ballot_submission')] + constraints = [UniqueConstraint(fields=['debate_team', 'preference', 'ballot_submission'])] verbose_name = _("debate team motion preference") verbose_name_plural = _("debate team motion preferences") @@ -80,8 +82,8 @@ class RoundMotion(models.Model): help_text=_("The order in which motions are displayed")) class Meta: + constraints = [UniqueConstraint(fields=['round', 'seq'])] ordering = ('round', 'seq') - unique_together = ('round', 'seq') verbose_name = _("round motion") verbose_name_plural = _("round motions") diff --git a/tabbycat/notifications/migrations/0013_alter_bulknotification_event.py b/tabbycat/notifications/migrations/0013_alter_bulknotification_event.py new file mode 100644 index 00000000000..e7f0c89077f --- /dev/null +++ b/tabbycat/notifications/migrations/0013_alter_bulknotification_event.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.4 on 2024-05-04 13:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('notifications', '0012_auto_20201018_2128'), + ] + + operations = [ + migrations.AlterField( + model_name='bulknotification', + name='event', + field=models.CharField(choices=[('p', 'team points'), ('c', 'ballot confirmed'), ('f', 'feedback URL'), ('b', 'ballot URL'), ('u', 'landing page URL'), ('d', 'adjudicator draw released'), ('t', 'team registration'), ('a', 'adjudicator registration'), ('m', 'motion(s) released'), ('r', 'team draw released'), ('', 'custom message')], max_length=20, verbose_name='event'), + ), + ] diff --git a/tabbycat/notifications/views.py b/tabbycat/notifications/views.py index ca5e2ff28f0..a7651d0b50d 100644 --- a/tabbycat/notifications/views.py +++ b/tabbycat/notifications/views.py @@ -195,7 +195,7 @@ def post(self, request: 'HttpRequest', *args, **kwargs) -> HttpResponse: for obj in data: dt = datetime.fromtimestamp(obj['timestamp']) - timestamp = timezone.make_aware(dt, timezone.utc) + timestamp = timezone.make_aware(dt, datetime.timezone.utc) email_id = record_lookup.get(obj['hook-id'], None) if email_id is None: continue diff --git a/tabbycat/participants/migrations/0022_rename_team_tournament_institution_short_reference_participant_tournam_160efa_idx_and_more.py b/tabbycat/participants/migrations/0022_rename_team_tournament_institution_short_reference_participant_tournam_160efa_idx_and_more.py new file mode 100644 index 00000000000..e958134c7e6 --- /dev/null +++ b/tabbycat/participants/migrations/0022_rename_team_tournament_institution_short_reference_participant_tournam_160efa_idx_and_more.py @@ -0,0 +1,60 @@ +# Generated by Django 5.0.4 on 2024-05-04 13:21 + +import utils.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('adjallocation', '0010_alter_adjudicatoradjudicatorconflict_unique_together_and_more'), + ('breakqual', '0006_alter_breakcategory_unique_together_and_more'), + ('participants', '0021_team_seed'), + ('tournaments', '0011_alter_round_unique_together_and_more'), + ] + + operations = [ + migrations.RenameIndex( + model_name='team', + new_name='participant_tournam_160efa_idx', + old_fields=('tournament', 'institution', 'short_reference'), + ), + migrations.AlterUniqueTogether( + name='speakercategory', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='team', + unique_together=set(), + ), + migrations.AlterIndexTogether( + name='speakercategory', + index_together=set(), + ), + migrations.AlterField( + model_name='person', + name='code_name', + field=models.CharField(blank=True, help_text='Name used to obscure real name on public-facing pages', max_length=25, verbose_name='code name'), + ), + migrations.AlterField( + model_name='team', + name='emoji', + field=models.CharField(blank=True, choices=[('โ˜บ๏ธ', 'โ˜บ๏ธ White Smiling'), ('โ˜น', 'โ˜น White Frowning'), ('โ˜๏ธ', 'โ˜๏ธ White Up Pointing Index'), ('โœŒ๏ธ', 'โœŒ๏ธ Victory Hand'), ('โœ', 'โœ Writing Hand'), ('โค๏ธ', 'โค๏ธ Heavy Black Heart'), ('โฃ', 'โฃ Heart Exclamation Mark'), ('โ˜ ', 'โ˜  Skull and Crossbones'), ('โ™จ๏ธ', 'โ™จ๏ธ Hot Springs'), ('โœˆ๏ธ', 'โœˆ๏ธ Airplane'), ('โŒ›', 'โŒ› Hourglass'), ('โŒš', 'โŒš Watch'), ('โ™ˆ', 'โ™ˆ Aries'), ('โ™‰', 'โ™‰ Taurus'), ('โ™Š', 'โ™Š Gemini'), ('โ™‹', 'โ™‹ Cancer'), ('โ™Œ', 'โ™Œ Leo'), ('โ™', 'โ™ Virgo'), ('โ™Ž', 'โ™Ž Libra'), ('โ™', 'โ™ Scorpius'), ('โ™', 'โ™ Sagittarius'), ('โ™‘', 'โ™‘ Capricorn'), ('โ™’', 'โ™’ Aquarius'), ('โ™“', 'โ™“ Pisces'), ('โ˜€๏ธ', 'โ˜€๏ธ Black Sun With Rays'), ('โ˜๏ธ', 'โ˜๏ธ Cloud'), ('โ˜‚', 'โ˜‚ Umbrella'), ('โ„๏ธ', 'โ„๏ธ Snowflake'), ('โ˜ƒ', 'โ˜ƒ Snowman'), ('โ˜„๏ธ', 'โ˜„๏ธ Comet'), ('โ™ ๏ธ', 'โ™ ๏ธ Spade Suit'), ('โ™ฅ๏ธ', 'โ™ฅ๏ธ Heart Suit'), ('โ™ฆ๏ธ', 'โ™ฆ๏ธ Diamond Suit'), ('โ™ฃ๏ธ', 'โ™ฃ๏ธ Club Suit'), ('โ–ถ๏ธ', 'โ–ถ๏ธ Black Right-Pointing Triangle'), ('โ—€๏ธ', 'โ—€๏ธ Black Left-Pointing Triangle'), ('โ˜Ž๏ธ', 'โ˜Ž๏ธ Black Telephone'), ('โŒจ', 'โŒจ Keyboard'), ('โœ‰๏ธ', 'โœ‰๏ธ Envelope'), ('โœ๏ธ', 'โœ๏ธ Pencil'), ('โœ’๏ธ', 'โœ’๏ธ Black Nib'), ('โœ‚๏ธ', 'โœ‚๏ธ Scissors'), ('โ†—๏ธ', 'โ†—๏ธ North East Arrow'), ('โžก๏ธ', 'โžก๏ธ Black Rightwards Arrow'), ('โ†˜๏ธ', 'โ†˜๏ธ South East Arrow'), ('โ†™๏ธ', 'โ†™๏ธ South West Arrow'), ('โ†–๏ธ', 'โ†–๏ธ North West Arrow'), ('โ†•๏ธ', 'โ†•๏ธ Up Down Arrow'), ('โ†”๏ธ', 'โ†”๏ธ Left Right Arrow'), ('โ†ฉ๏ธ', 'โ†ฉ๏ธ Leftwards Arrow With Hook'), ('โ†ช๏ธ', 'โ†ช๏ธ Rightwards Arrow With Hook'), ('โœก', 'โœก Star of David'), ('โ˜ธ', 'โ˜ธ Wheel of Dharma'), ('โ˜ฏ', 'โ˜ฏ Yin Yang'), ('โœ', 'โœ Latin Cross'), ('โ˜ฆ', 'โ˜ฆ Orthodox Cross'), ('โ˜ช', 'โ˜ช Star and Crescent'), ('โ˜ฎ', 'โ˜ฎ Peace Symbol'), ('โ˜ข', 'โ˜ข Radioactive Sign'), ('โ˜ฃ', 'โ˜ฃ Biohazard Sign'), ('โ˜‘๏ธ', 'โ˜‘๏ธ Ballot Box With Check'), ('โœ”๏ธ', 'โœ”๏ธ Heavy Check Mark'), ('โœ–๏ธ', 'โœ–๏ธ Heavy Multiplication X'), ('โœณ๏ธ', 'โœณ๏ธ Eight Spoked Asterisk'), ('โœด๏ธ', 'โœด๏ธ Eight Pointed Black Star'), ('โ‡๏ธ', 'โ‡๏ธ Sparkle'), ('โ€ผ๏ธ', 'โ€ผ๏ธ Double Exclamation Mark'), ('ใ€ฐ๏ธ', 'ใ€ฐ๏ธ Wavy Dash'), ('ยฉ๏ธ', 'ยฉ๏ธ Copyright Sign'), ('ยฎ๏ธ', 'ยฎ๏ธ Registered Sign'), ('โ„ข๏ธ', 'โ„ข๏ธ Trade Mark Sign'), ('โ“‚๏ธ', 'โ“‚๏ธ Capital M'), ('ใŠ—๏ธ', 'ใŠ—๏ธ Congratulations'), ('ใŠ™๏ธ', 'ใŠ™๏ธ Secret'), ('โ–ช๏ธ', 'โ–ช๏ธ Black Square'), ('โ–ซ๏ธ', 'โ–ซ๏ธ White Square'), ('#โƒฃ๏ธ', '#โƒฃ๏ธ Keycap Number Sign'), ('*โƒฃ', '*โƒฃ Keycap Asterisk'), ('0โƒฃ๏ธ', '0โƒฃ๏ธ Keycap Digit Zero'), ('1โƒฃ๏ธ', '1โƒฃ๏ธ Keycap Digit One'), ('2โƒฃ๏ธ', '2โƒฃ๏ธ Keycap Digit Two'), ('3โƒฃ๏ธ', '3โƒฃ๏ธ Keycap Digit Three'), ('4โƒฃ๏ธ', '4โƒฃ๏ธ Keycap Digit Four'), ('5โƒฃ๏ธ', '5โƒฃ๏ธ Keycap Digit Five'), ('6โƒฃ๏ธ', '6โƒฃ๏ธ Keycap Digit Six'), ('7โƒฃ๏ธ', '7โƒฃ๏ธ Keycap Digit Seven'), ('8โƒฃ๏ธ', '8โƒฃ๏ธ Keycap Digit Eight'), ('9โƒฃ๏ธ', '9โƒฃ๏ธ Keycap Digit Nine'), ('โ‰๏ธ', 'โ‰๏ธ Exclamation Question Mark'), ('โ„น๏ธ', 'โ„น๏ธ Information Source'), ('โคด๏ธ', 'โคด๏ธ Right-Curve-Up'), ('โคต๏ธ', 'โคต๏ธ Right-Curve-Down'), ('โ™ป๏ธ', 'โ™ป๏ธ Recycling'), ('ใ€ฝ๏ธ', 'ใ€ฝ๏ธ Part Alternation Mark'), ('โ—ป๏ธ', 'โ—ป๏ธ White Medium Square'), ('โ—ผ๏ธ', 'โ—ผ๏ธ Black Medium Square'), ('โ—ฝ', 'โ—ฝ White Medium Small Square'), ('โ—พ', 'โ—พ Black Medium Small Square'), ('โ˜•', 'โ˜• Hot Beverage'), ('โš ๏ธ', 'โš ๏ธ Warning Sign'), ('โ˜”', 'โ˜” Umbrella With Rain Drops'), ('โ', 'โ Eject Symbol'), ('โฌ†๏ธ', 'โฌ†๏ธ Upwards Black Arrow'), ('โฌ‡๏ธ', 'โฌ‡๏ธ Downwards Black Arrow'), ('โฌ…๏ธ', 'โฌ…๏ธ Leftwards Black Arrow'), ('โšก', 'โšก High Voltage'), ('โ˜˜', 'โ˜˜ Shamrock'), ('โš“', 'โš“ Anchor'), ('โ™ฟ', 'โ™ฟ Wheelchair Symbol'), ('โš’', 'โš’ Hammer and Pick'), ('โš™', 'โš™ Gear'), ('โš—', 'โš— Alembic'), ('โš–', 'โš– Scales'), ('โš”', 'โš” Crossed Swords'), ('โšฐ', 'โšฐ Coffin'), ('โšฑ', 'โšฑ Funeral Urn'), ('โšœ', 'โšœ Fleur-De-Lis'), ('โš›', 'โš› Atom Symbol'), ('โšช', 'โšช Medium White Circle'), ('โšซ', 'โšซ Medium Black Circle'), ('๐Ÿ€„', '๐Ÿ€„ Mahjong Tile Red Dragon'), ('โญ', 'โญ White Medium Star'), ('โฌ›', 'โฌ› Black Square'), ('โฌœ', 'โฌœ White Square'), ('โ›‘', 'โ›‘ Rescue Hat'), ('โ›ฐ', 'โ›ฐ Mountain'), ('โ›ช', 'โ›ช Church'), ('โ›ฒ', 'โ›ฒ Fountain'), ('โ›บ', 'โ›บ Tent'), ('โ›ฝ', 'โ›ฝ Fuel Pump'), ('โ›ต', 'โ›ต Sailboat'), ('โ›ด', 'โ›ด Ferry'), ('โ›”', 'โ›” No Entry'), ('โ›…', 'โ›… Overcast'), ('โ›ˆ', 'โ›ˆ Storm'), ('โ›ฑ', 'โ›ฑ Umbrella'), ('โ›„', 'โ›„ Snowman'), ('โšฝ', 'โšฝ Soccer'), ('โšพ', 'โšพ Baseball'), ('โ›ณ', 'โ›ณ Hole in One'), ('โ›ธ', 'โ›ธ Ice Skate'), ('โ›ท', 'โ›ท Skier'), ('โ›น', 'โ›น Person With Ball'), ('โ›', 'โ› Pick'), ('โ›“', 'โ›“ Chains'), ('โ›ฉ', 'โ›ฉ Shinto Shrine'), ('โญ•', 'โญ• Heavy Large Circle'), ('โ—', 'โ— Heavy Exclamation Mark'), ('๐Ÿ…ฟ๏ธ', '๐Ÿ…ฟ๏ธ Squared P'), ('๐Ÿˆฏ', '๐Ÿˆฏ Squared ๆŒ‡ (Finger)'), ('๐Ÿˆš', '๐Ÿˆš Squared CJK Unified Ideograph-7121'), ('๐Ÿ˜', '๐Ÿ˜ Smiling Eyes'), ('๐Ÿ˜‚', '๐Ÿ˜‚ Joy Tears'), ('๐Ÿ˜ƒ', '๐Ÿ˜ƒ Smiling Face With Open Mouth'), ('๐Ÿ˜„', '๐Ÿ˜„ Smiling Face With Open Mouth and Smiling Eyes'), ('๐Ÿ˜…', '๐Ÿ˜… Cold Sweat'), ('๐Ÿ˜†', '๐Ÿ˜† Closed Eyes'), ('๐Ÿ˜‰', '๐Ÿ˜‰ Winky'), ('๐Ÿ˜Š', '๐Ÿ˜Š Smiling Eyes'), ('๐Ÿ˜‹', '๐Ÿ˜‹ Face Savouring Delicious Food'), ('๐Ÿ˜Ž', '๐Ÿ˜Ž Shaded Eyes'), ('๐Ÿ˜', '๐Ÿ˜ Heart Eyes'), ('๐Ÿ˜˜', '๐Ÿ˜˜ Kissy'), ('๐Ÿ˜š', '๐Ÿ˜š Kissing Face With Closed Eyes'), ('๐Ÿ˜‡', '๐Ÿ˜‡ Halo'), ('๐Ÿ˜', '๐Ÿ˜ Neutral'), ('๐Ÿ˜ถ', '๐Ÿ˜ถ No Mouth'), ('๐Ÿ˜', '๐Ÿ˜ Smirking'), ('๐Ÿ˜ฃ', '๐Ÿ˜ฃ Persevering'), ('๐Ÿ˜ฅ', '๐Ÿ˜ฅ Disappointed'), ('๐Ÿ˜ช', '๐Ÿ˜ช Sleepy'), ('๐Ÿ˜ซ', '๐Ÿ˜ซ Tired'), ('๐Ÿ˜Œ', '๐Ÿ˜Œ Relieved'), ('๐Ÿ˜œ', '๐Ÿ˜œ Tongue Out'), ('๐Ÿ˜', '๐Ÿ˜ Tongue Out Closed Eyes'), ('๐Ÿ˜’', '๐Ÿ˜’ Unamused'), ('๐Ÿ˜“', '๐Ÿ˜“ Cold Sweat'), ('๐Ÿ˜”', '๐Ÿ˜” Pensive'), ('๐Ÿ˜–', '๐Ÿ˜– Confounded'), ('๐Ÿ˜ท', '๐Ÿ˜ท Medical Mask'), ('๐Ÿ˜ฒ', '๐Ÿ˜ฒ Astonished'), ('๐Ÿ˜ž', '๐Ÿ˜ž Disappointed'), ('๐Ÿ˜ค', '๐Ÿ˜ค Face With Look of Triumph'), ('๐Ÿ˜ข', '๐Ÿ˜ข Crying'), ('๐Ÿ˜ญ', '๐Ÿ˜ญ Sobbing'), ('๐Ÿ˜จ', '๐Ÿ˜จ Fearful'), ('๐Ÿ˜ฉ', '๐Ÿ˜ฉ Weary'), ('๐Ÿ˜ฐ', '๐Ÿ˜ฐ Open Mouth Cold Sweat'), ('๐Ÿ˜ฑ', '๐Ÿ˜ฑ Screaming'), ('๐Ÿ˜ณ', '๐Ÿ˜ณ Flushed'), ('๐Ÿ˜ต', '๐Ÿ˜ต Dizzy'), ('๐Ÿ˜ก', '๐Ÿ˜ก Pouting'), ('๐Ÿ˜ ', '๐Ÿ˜  Angry'), ('๐Ÿ‘ฟ', '๐Ÿ‘ฟ Imp'), ('๐Ÿ˜ˆ', '๐Ÿ˜ˆ Smiling Face With Horns'), ('๐Ÿ‘ฆ', '๐Ÿ‘ฆ Boy'), ('๐Ÿ‘ง', '๐Ÿ‘ง Girl'), ('๐Ÿ‘จ', '๐Ÿ‘จ Generic Man'), ('๐Ÿ‘ฉ', '๐Ÿ‘ฉ Generic Woman'), ('๐Ÿ‘ด', '๐Ÿ‘ด Older Man'), ('๐Ÿ‘ต', '๐Ÿ‘ต Older Woman'), ('๐Ÿ‘ถ', '๐Ÿ‘ถ Baby'), ('๐Ÿ‘ฑ', '๐Ÿ‘ฑ Person With Blond Hair'), ('๐Ÿ‘ฎ', '๐Ÿ‘ฎ Police Officer'), ('๐Ÿ‘ฒ', '๐Ÿ‘ฒ Man With Gua Pi Mao'), ('๐Ÿ‘ณ', '๐Ÿ‘ณ Man With Turban'), ('๐Ÿ‘ท', '๐Ÿ‘ท Trade Worker'), ('๐Ÿ‘ธ', '๐Ÿ‘ธ Princess'), ('๐Ÿ’‚', '๐Ÿ’‚ Guardsman'), ('๐ŸŽ…', '๐ŸŽ… Santa Claus'), ('๐Ÿ‘ผ', '๐Ÿ‘ผ Baby Angel'), ('๐Ÿ‘ฏ', '๐Ÿ‘ฏ Bunny Women'), ('๐Ÿ’†', '๐Ÿ’† Face Massage'), ('๐Ÿ’‡', '๐Ÿ’‡ Haircut'), ('๐Ÿ‘ฐ', '๐Ÿ‘ฐ Bride'), ('๐Ÿ™', '๐Ÿ™ Person Frowning'), ('๐Ÿ™Ž', '๐Ÿ™Ž Person With Pouting'), ('๐Ÿ™…', '๐Ÿ™… Block Gesture'), ('๐Ÿ™†', '๐Ÿ™† OK Gesture'), ('๐Ÿ’', '๐Ÿ’ Sass Gesture'), ('๐Ÿ™‹', '๐Ÿ™‹ Raised Hand'), ('๐Ÿ™‡', '๐Ÿ™‡ Deep Bow'), ('๐Ÿ™Œ', '๐Ÿ™Œ Praise Hands'), ('๐Ÿ™', '๐Ÿ™ Prayer Hands'), ('๐Ÿ‘ค', '๐Ÿ‘ค Bust in Silhouette'), ('๐Ÿ‘ฅ', '๐Ÿ‘ฅ Busts in Silhouette'), ('๐Ÿšถ', '๐Ÿšถ Pedestrian'), ('๐Ÿƒ', '๐Ÿƒ Runner'), ('๐Ÿ’ƒ', '๐Ÿ’ƒ Dancer'), ('๐Ÿ’', '๐Ÿ’ Kiss'), ('๐Ÿ’‘', '๐Ÿ’‘ Heteronormative Couple'), ('๐Ÿ‘ช', '๐Ÿ‘ช Hetero Family'), ('๐Ÿ‘ซ', '๐Ÿ‘ซ Man & Woman'), ('๐Ÿ‘ฌ', '๐Ÿ‘ฌ Two Men'), ('๐Ÿ‘ญ', '๐Ÿ‘ญ Two Women'), ('๐Ÿ’ช', '๐Ÿ’ช Biceps'), ('๐Ÿ‘ˆ', '๐Ÿ‘ˆ Left Pointing Backhand'), ('๐Ÿ‘‰', '๐Ÿ‘‰ Right Pointing Backhand'), ('๐Ÿ‘†', '๐Ÿ‘† Pointing Hand'), ('๐Ÿ‘‡', '๐Ÿ‘‡ Down Pointing Backhand'), ('โœŠ', 'โœŠ Power Hand'), ('โœ‹', 'โœ‹ Palm Hand'), ('๐Ÿ‘Š', '๐Ÿ‘Š Fist Hand'), ('๐Ÿ‘Œ', '๐Ÿ‘Œ OK Hand'), ('๐Ÿ‘', '๐Ÿ‘ Thumbs Up'), ('๐Ÿ‘Ž', '๐Ÿ‘Ž Thumbs Down'), ('๐Ÿ‘‹', '๐Ÿ‘‹ Waving Hand Sign'), ('๐Ÿ‘', '๐Ÿ‘ Clappy Hands'), ('๐Ÿ‘', '๐Ÿ‘ Open Hands Sign'), ('๐Ÿ’…', '๐Ÿ’… Nail Polish'), ('๐Ÿ‘ฃ', '๐Ÿ‘ฃ Footprints'), ('๐Ÿ‘€', '๐Ÿ‘€ Eyes'), ('๐Ÿ‘‚', '๐Ÿ‘‚ Ear'), ('๐Ÿ‘ƒ', '๐Ÿ‘ƒ Nose'), ('๐Ÿ‘…', '๐Ÿ‘… Lick'), ('๐Ÿ‘„', '๐Ÿ‘„ Mouth'), ('๐Ÿ’‹', '๐Ÿ’‹ Kiss Mark'), ('๐Ÿ’˜', '๐Ÿ’˜ Cupid Arrow'), ('๐Ÿ’“', '๐Ÿ’“ Beating Heart'), ('๐Ÿ’”', '๐Ÿ’” Broken Heart'), ('๐Ÿ’•', '๐Ÿ’• Two Hearts'), ('๐Ÿ’–', '๐Ÿ’– Sparkly Heart'), ('๐Ÿ’—', '๐Ÿ’— Growing Heart'), ('๐Ÿ’™', '๐Ÿ’™ Blue Heart'), ('๐Ÿ’š', '๐Ÿ’š Green Heart'), ('๐Ÿ’›', '๐Ÿ’› Yellow Heart'), ('๐Ÿ’œ', '๐Ÿ’œ Purple Heart'), ('๐Ÿ’', '๐Ÿ’ Heart With Ribbon'), ('๐Ÿ’ž', '๐Ÿ’ž Revolving Hearts'), ('๐Ÿ’Ÿ', '๐Ÿ’Ÿ Heart Decoration'), ('๐Ÿ’Œ', '๐Ÿ’Œ Love Letter'), ('๐Ÿ’ง', '๐Ÿ’ง Droplet'), ('๐Ÿ’ค', '๐Ÿ’ค ZZZ'), ('๐Ÿ’ข', '๐Ÿ’ข Anger'), ('๐Ÿ’ฃ', '๐Ÿ’ฃ Bomb'), ('๐Ÿ’ฅ', '๐Ÿ’ฅ Sparks'), ('๐Ÿ’ฆ', '๐Ÿ’ฆ Splashing'), ('๐Ÿ’จ', '๐Ÿ’จ Dash'), ('๐Ÿ’ซ', '๐Ÿ’ซ Shooting Star'), ('๐Ÿ’ฌ', '๐Ÿ’ฌ Speech Bubble'), ('๐Ÿ’ญ', '๐Ÿ’ญ Thinky Cloud'), ('๐Ÿ‘“', '๐Ÿ‘“ Eyeglasses'), ('๐Ÿ‘”', '๐Ÿ‘” Business Casual'), ('๐Ÿ‘•', '๐Ÿ‘• T-Shirt'), ('๐Ÿ‘–', '๐Ÿ‘– Jeans'), ('๐Ÿ‘—', '๐Ÿ‘— Dress'), ('๐Ÿ‘˜', '๐Ÿ‘˜ Kimono'), ('๐Ÿ‘™', '๐Ÿ‘™ Bikini'), ('๐Ÿ‘š', '๐Ÿ‘š Womans Clothes'), ('๐Ÿ‘›', '๐Ÿ‘› Purse'), ('๐Ÿ‘œ', '๐Ÿ‘œ Handbag'), ('๐Ÿ‘', '๐Ÿ‘ Pouch'), ('๐ŸŽ’', '๐ŸŽ’ Backpack'), ('๐Ÿ‘ž', '๐Ÿ‘ž Mans Shoe'), ('๐Ÿ‘Ÿ', '๐Ÿ‘Ÿ Running Shoe'), ('๐Ÿ‘ ', '๐Ÿ‘  Heels'), ('๐Ÿ‘ก', '๐Ÿ‘ก Womans Sandal'), ('๐Ÿ‘ข', '๐Ÿ‘ข Womans Boots'), ('๐Ÿ‘‘', '๐Ÿ‘‘ Crown'), ('๐Ÿ‘’', "๐Ÿ‘’ Lady's Hat"), ('๐ŸŽฉ', '๐ŸŽฉ Top Hat'), ('๐Ÿ’„', '๐Ÿ’„ Lipstick'), ('๐Ÿ’', '๐Ÿ’ Proposal'), ('๐Ÿ’Ž', '๐Ÿ’Ž Gem'), ('๐Ÿ‘น', '๐Ÿ‘น Japanese Ogre'), ('๐Ÿ‘บ', '๐Ÿ‘บ Japanese Goblin'), ('๐Ÿ‘ป', '๐Ÿ‘ป Ghost'), ('๐Ÿ’€', '๐Ÿ’€ Skull'), ('๐Ÿ‘ฝ', '๐Ÿ‘ฝ Alien'), ('๐Ÿ‘พ', '๐Ÿ‘พ Space Invader'), ('๐Ÿ’ฉ', '๐Ÿ’ฉ Pile of Poo'), ('๐Ÿต', '๐Ÿต Monkey'), ('๐Ÿ™ˆ', '๐Ÿ™ˆ See No Evil'), ('๐Ÿ™‰', '๐Ÿ™‰ Hear No Evil'), ('๐Ÿ™Š', '๐Ÿ™Š Speak No Evil'), ('๐Ÿ’', '๐Ÿ’ Monkey'), ('๐Ÿถ', '๐Ÿถ Dog'), ('๐Ÿ•', '๐Ÿ• Dog'), ('๐Ÿฉ', '๐Ÿฉ Poodle'), ('๐Ÿบ', '๐Ÿบ Wolf'), ('๐Ÿฑ', '๐Ÿฑ Cat'), ('๐Ÿ˜ธ', '๐Ÿ˜ธ Grinning Cat with Smiling Eyes'), ('๐Ÿ˜น', '๐Ÿ˜น Cat with Tears of Joy'), ('๐Ÿ˜บ', '๐Ÿ˜บ Smiling Cat with Open Mouth'), ('๐Ÿ˜ป', '๐Ÿ˜ป Smiling Cat with Heart Eyes'), ('๐Ÿ˜ผ', '๐Ÿ˜ผ Cat with Wry Smile'), ('๐Ÿ˜ฝ', '๐Ÿ˜ฝ Kissing Cat with Closed Eyes'), ('๐Ÿ˜พ', '๐Ÿ˜พ Pouting Cat Face'), ('๐Ÿ˜ฟ', '๐Ÿ˜ฟ Crying Cat Face'), ('๐Ÿ™€', '๐Ÿ™€ Weary Cat Face'), ('๐Ÿˆ', '๐Ÿˆ Cat'), ('๐Ÿฏ', '๐Ÿฏ Tiger'), ('๐Ÿ…', '๐Ÿ… Tiger'), ('๐Ÿ†', '๐Ÿ† Leopard'), ('๐Ÿด', '๐Ÿด Horse'), ('๐ŸŽ', '๐ŸŽ Horse'), ('๐Ÿฎ', '๐Ÿฎ Cow'), ('๐Ÿ‚', '๐Ÿ‚ Ox'), ('๐Ÿƒ', '๐Ÿƒ Water Buffalo'), ('๐Ÿ„', '๐Ÿ„ Cow'), ('๐Ÿท', '๐Ÿท Pig'), ('๐Ÿ–', '๐Ÿ– Pig'), ('๐Ÿ—', '๐Ÿ— Boar'), ('๐Ÿฝ', '๐Ÿฝ Pig Nose'), ('๐Ÿ', '๐Ÿ Ram'), ('๐Ÿ‘', '๐Ÿ‘ Sheep'), ('๐Ÿ', '๐Ÿ Goat'), ('๐Ÿช', '๐Ÿช Dromedary Camel'), ('๐Ÿซ', '๐Ÿซ Bactrian Camel'), ('๐Ÿ˜', '๐Ÿ˜ Elephant'), ('๐Ÿญ', '๐Ÿญ Mouse'), ('๐Ÿ', '๐Ÿ Mouse'), ('๐Ÿ€', '๐Ÿ€ Rat'), ('๐Ÿน', '๐Ÿน Hamster'), ('๐Ÿฐ', '๐Ÿฐ Rabbit'), ('๐Ÿ‡', '๐Ÿ‡ Rabbit'), ('๐Ÿป', '๐Ÿป Bear'), ('๐Ÿจ', '๐Ÿจ Koala'), ('๐Ÿผ', '๐Ÿผ Panda'), ('๐Ÿพ', '๐Ÿพ Paw Prints'), ('๐Ÿ”', '๐Ÿ” Chicken'), ('๐Ÿ“', '๐Ÿ“ Rooster'), ('๐Ÿฃ', '๐Ÿฃ Hatching'), ('๐Ÿค', '๐Ÿค Chick'), ('๐Ÿฅ', '๐Ÿฅ Front-Facing Baby Chick'), ('๐Ÿฆ', '๐Ÿฆ Bird'), ('๐Ÿง', '๐Ÿง Penguin'), ('๐Ÿธ', '๐Ÿธ Frog'), ('๐ŸŠ', '๐ŸŠ Croc'), ('๐Ÿข', '๐Ÿข Turtle'), ('๐Ÿ', '๐Ÿ Slithering'), ('๐Ÿฒ', '๐Ÿฒ Dragon'), ('๐Ÿ‰', '๐Ÿ‰ Dragon'), ('๐Ÿณ', '๐Ÿณ Whale'), ('๐Ÿ‹', '๐Ÿ‹ Whale'), ('๐Ÿฌ', '๐Ÿฌ Dolphin'), ('๐ŸŸ', '๐ŸŸ Fish'), ('๐Ÿ ', '๐Ÿ  Fish'), ('๐Ÿก', '๐Ÿก Blowfish'), ('๐Ÿ™', '๐Ÿ™ Octopus'), ('๐Ÿš', '๐Ÿš Shell'), ('๐ŸŒ', '๐ŸŒ Snail'), ('๐Ÿ›', '๐Ÿ› Bug'), ('๐Ÿœ', '๐Ÿœ Ant'), ('๐Ÿ', '๐Ÿ Honeybee'), ('๐Ÿž', '๐Ÿž Lady Beetle'), ('๐Ÿ’', '๐Ÿ’ Bouquet'), ('๐ŸŒธ', '๐ŸŒธ Sakura'), ('๐Ÿ’ฎ', '๐Ÿ’ฎ White Flower'), ('๐ŸŒน', '๐ŸŒน Rose'), ('๐ŸŒบ', '๐ŸŒบ Hibiscus'), ('๐ŸŒป', '๐ŸŒป Sunflower'), ('๐ŸŒผ', '๐ŸŒผ Blossom'), ('๐ŸŒท', '๐ŸŒท Tulip'), ('๐ŸŒฑ', '๐ŸŒฑ Seedling'), ('๐ŸŒฒ', '๐ŸŒฒ Evergreen Tree'), ('๐ŸŒณ', '๐ŸŒณ Deciduous Tree'), ('๐ŸŒด', '๐ŸŒด Palm Tree'), ('๐ŸŒต', '๐ŸŒต Cactus'), ('๐ŸŒพ', '๐ŸŒพ Ear of Rice'), ('๐ŸŒฟ', '๐ŸŒฟ Herb'), ('๐Ÿ€', '๐Ÿ€ Clover'), ('๐Ÿ', '๐Ÿ Maple Leaf'), ('๐Ÿ‚', '๐Ÿ‚ Fallen Leaf'), ('๐Ÿƒ', '๐Ÿƒ Blown Leaves'), ('๐Ÿ‡', '๐Ÿ‡ Grapes'), ('๐Ÿˆ', '๐Ÿˆ Melon'), ('๐Ÿ‰', '๐Ÿ‰ Watermelon'), ('๐ŸŠ', '๐ŸŠ Tangerine'), ('๐Ÿ‹', '๐Ÿ‹ Lemon'), ('๐ŸŒ', '๐ŸŒ Banana'), ('๐Ÿ', '๐Ÿ Pineapple'), ('๐ŸŽ', '๐ŸŽ Red Apple'), ('๐Ÿ', '๐Ÿ Green Apple'), ('๐Ÿ', '๐Ÿ Pear'), ('๐Ÿ‘', '๐Ÿ‘ Peach'), ('๐Ÿ’', '๐Ÿ’ Cherries'), ('๐Ÿ“', '๐Ÿ“ Strawberry'), ('๐Ÿ…', '๐Ÿ… Tomato'), ('๐Ÿ†', '๐Ÿ† Eggplant'), ('๐ŸŒฝ', '๐ŸŒฝ Corn'), ('๐Ÿ„', '๐Ÿ„ Mushroom'), ('๐ŸŒฐ', '๐ŸŒฐ Chestnut'), ('๐Ÿž', '๐Ÿž Bread'), ('๐Ÿ–', '๐Ÿ– Meat on Bone'), ('๐Ÿ—', '๐Ÿ— Poultry Leg'), ('๐Ÿ”', '๐Ÿ” Hamburger'), ('๐ŸŸ', '๐ŸŸ Fries'), ('๐Ÿ•', '๐Ÿ• Pizza'), ('๐Ÿฒ', '๐Ÿฒ Pot of Food'), ('๐Ÿฑ', '๐Ÿฑ Bento Box'), ('๐Ÿ˜', '๐Ÿ˜ Rice Cracker'), ('๐Ÿ™', '๐Ÿ™ Rice Ball'), ('๐Ÿš', '๐Ÿš Cooked Rice'), ('๐Ÿ›', '๐Ÿ› Curry and Rice'), ('๐Ÿœ', '๐Ÿœ Steaming Bowl'), ('๐Ÿ', '๐Ÿ Spaghetti'), ('๐Ÿ ', '๐Ÿ  Sweet Potato'), ('๐Ÿข', '๐Ÿข Oden'), ('๐Ÿฃ', '๐Ÿฃ Sushi'), ('๐Ÿค', '๐Ÿค Fried Shrimp'), ('๐Ÿฅ', '๐Ÿฅ Fish Cake With Swirl Design'), ('๐Ÿก', '๐Ÿก Dango'), ('๐Ÿฆ', '๐Ÿฆ Ice Cream'), ('๐Ÿง', '๐Ÿง Shaved Ice'), ('๐Ÿจ', '๐Ÿจ Ice Cream'), ('๐Ÿฉ', '๐Ÿฉ Doughnut'), ('๐Ÿช', '๐Ÿช Cookie'), ('๐ŸŽ‚', '๐ŸŽ‚ Birthday Cake'), ('๐Ÿฐ', '๐Ÿฐ Shortcake'), ('๐Ÿซ', '๐Ÿซ Chocolate Bar'), ('๐Ÿฌ', '๐Ÿฌ Candy'), ('๐Ÿญ', '๐Ÿญ Lollipop'), ('๐Ÿฎ', '๐Ÿฎ Custard'), ('๐Ÿฏ', '๐Ÿฏ Honey Pot'), ('๐Ÿผ', '๐Ÿผ Baby Bottle'), ('๐Ÿต', '๐Ÿต Teacup Without Handle'), ('๐Ÿถ', '๐Ÿถ Sake Bottle and Cup'), ('๐Ÿท', '๐Ÿท Wine Glass'), ('๐Ÿธ', '๐Ÿธ Cocktail Glass'), ('๐Ÿน', '๐Ÿน Tropical Drink'), ('๐Ÿบ', '๐Ÿบ Beer'), ('๐Ÿป', '๐Ÿป Clinking Beer Mugs'), ('๐Ÿด', '๐Ÿด Fork & Knife'), ('๐Ÿณ', '๐Ÿณ Cooking'), ('๐ŸŒ', '๐ŸŒ Earth Globe Europe-Africa'), ('๐ŸŒŽ', '๐ŸŒŽ Earth Globe Americas'), ('๐ŸŒ', '๐ŸŒ Earth Globe Asia-Australia'), ('๐ŸŒ', '๐ŸŒ Globe With Meridians'), ('๐ŸŒ‹', '๐ŸŒ‹ Volcano'), ('๐Ÿ—ป', '๐Ÿ—ป Mount Fuji'), ('๐Ÿ ', '๐Ÿ  House'), ('๐Ÿก', '๐Ÿก House With Garden'), ('๐Ÿข', '๐Ÿข Office'), ('๐Ÿฃ', '๐Ÿฃ Japanese Post Office'), ('๐Ÿค', '๐Ÿค European Post Office'), ('๐Ÿฅ', '๐Ÿฅ Hospital'), ('๐Ÿฆ', '๐Ÿฆ Bank'), ('๐Ÿจ', '๐Ÿจ Hotel'), ('๐Ÿฉ', '๐Ÿฉ Love Hotel'), ('๐Ÿช', '๐Ÿช Convenience Store'), ('๐Ÿซ', '๐Ÿซ School'), ('๐Ÿฌ', '๐Ÿฌ Department Store'), ('๐Ÿญ', '๐Ÿญ Factory'), ('๐Ÿฏ', '๐Ÿฏ Japanese Castle'), ('๐Ÿฐ', '๐Ÿฐ Castle'), ('๐Ÿ’’', '๐Ÿ’’ Wedding'), ('๐Ÿ—ผ', '๐Ÿ—ผ Tokyo Tower'), ('๐Ÿ—ฝ', '๐Ÿ—ฝ Liberty'), ('๐Ÿ—พ', '๐Ÿ—พ Silhouette of Japan'), ('๐ŸŒ', '๐ŸŒ Foggy'), ('๐ŸŒƒ', '๐ŸŒƒ Night With Stars'), ('๐ŸŒ„', '๐ŸŒ„ Sunrise Over Mountains'), ('๐ŸŒ…', '๐ŸŒ… Sunrise'), ('๐ŸŒ†', '๐ŸŒ† Cityscape at Dusk'), ('๐ŸŒ‡', '๐ŸŒ‡ Sunset Over Buildings'), ('๐ŸŒ‰', '๐ŸŒ‰ Bridge at Night'), ('๐ŸŒŠ', '๐ŸŒŠ Big Wave'), ('๐Ÿ—ฟ', '๐Ÿ—ฟ Moyai'), ('๐ŸŒŒ', '๐ŸŒŒ Milky Way'), ('๐ŸŽ ', '๐ŸŽ  Carousel Horse'), ('๐ŸŽก', '๐ŸŽก Ferris Wheel'), ('๐ŸŽข', '๐ŸŽข Roller Coaster'), ('๐Ÿ’ˆ', '๐Ÿ’ˆ Barber Pole'), ('๐ŸŽช', '๐ŸŽช Circus Tent'), ('๐ŸŽญ', '๐ŸŽญ Performing Arts'), ('๐ŸŽจ', '๐ŸŽจ Palette'), ('๐ŸŽฐ', '๐ŸŽฐ Slot Machine'), ('๐Ÿš‚', '๐Ÿš‚ Steam Locomotive'), ('๐Ÿšƒ', '๐Ÿšƒ Railcar'), ('๐Ÿš„', '๐Ÿš„ Fast Train'), ('๐Ÿš…', '๐Ÿš… Fast Train with Bullet Nose'), ('๐Ÿš†', '๐Ÿš† Train'), ('๐Ÿš‡', '๐Ÿš‡ Metro'), ('๐Ÿšˆ', '๐Ÿšˆ Light Rail'), ('๐Ÿš‰', '๐Ÿš‰ Station'), ('๐ŸšŠ', '๐ŸšŠ Tram'), ('๐Ÿš', '๐Ÿš Monorail'), ('๐Ÿšž', '๐Ÿšž Mountain Railway'), ('๐Ÿš‹', '๐Ÿš‹ Tram Car'), ('๐ŸšŒ', '๐ŸšŒ Bus'), ('๐Ÿš', '๐Ÿš Bus'), ('๐ŸšŽ', '๐ŸšŽ Trolleybus'), ('๐Ÿš', '๐Ÿš Bus Stop'), ('๐Ÿš', '๐Ÿš Minibus'), ('๐Ÿš‘', '๐Ÿš‘ Ambulance'), ('๐Ÿš’', '๐Ÿš’ Fire Engine'), ('๐Ÿš“', '๐Ÿš“ Police Car'), ('๐Ÿš”', '๐Ÿš” Police Car'), ('๐Ÿš•', '๐Ÿš• Taxi'), ('๐Ÿš–', '๐Ÿš– Oncoming Taxi'), ('๐Ÿš—', '๐Ÿš— Automobile'), ('๐Ÿš˜', '๐Ÿš˜ Automobile'), ('๐Ÿš™', '๐Ÿš™ Recreational Vehicle'), ('๐Ÿšš', '๐Ÿšš Truck'), ('๐Ÿš›', '๐Ÿš› Articulated Lorry'), ('๐Ÿšœ', '๐Ÿšœ Tractor'), ('๐Ÿšฒ', '๐Ÿšฒ Bicycle'), ('๐Ÿšณ', '๐Ÿšณ No Bicycles'), ('๐Ÿšจ', '๐Ÿšจ Alert Light'), ('๐Ÿ”ฑ', '๐Ÿ”ฑ Trident'), ('๐Ÿšฃ', '๐Ÿšฃ Rowboat'), ('๐Ÿšค', '๐Ÿšค Speedboat'), ('๐Ÿšข', '๐Ÿšข Ship'), ('๐Ÿ’บ', '๐Ÿ’บ Seat'), ('๐Ÿš', '๐Ÿš Helicopter'), ('๐ŸšŸ', '๐ŸšŸ Suspension Railway'), ('๐Ÿš ', '๐Ÿš  Sky Tram'), ('๐Ÿšก', '๐Ÿšก Aerial Tramway'), ('๐Ÿš€', '๐Ÿš€ Rocket'), ('๐Ÿง', '๐Ÿง ATM'), ('๐Ÿšฎ', '๐Ÿšฎ Put Litter in Its Place'), ('๐Ÿšฅ', '๐Ÿšฅ Horizontal Traffic Light'), ('๐Ÿšฆ', '๐Ÿšฆ Traffic Light'), ('๐Ÿšง', '๐Ÿšง Hazard Sign'), ('๐Ÿšซ', '๐Ÿšซ Prohibited'), ('๐Ÿšญ', '๐Ÿšญ No Smoking'), ('๐Ÿšฏ', '๐Ÿšฏ Do Not Litter'), ('๐Ÿšฐ', '๐Ÿšฐ Tap Water'), ('๐Ÿšฑ', '๐Ÿšฑ Non-Potable Water'), ('๐Ÿšท', '๐Ÿšท No Pedestrians'), ('๐Ÿšธ', '๐Ÿšธ Children Crossing'), ('๐Ÿšน', '๐Ÿšน Mens Symbol'), ('๐Ÿšบ', '๐Ÿšบ Womens Symbol'), ('๐Ÿšป', '๐Ÿšป Restroom'), ('๐Ÿšผ', '๐Ÿšผ Baby Symbol'), ('๐Ÿšพ', '๐Ÿšพ Water Closet'), ('๐Ÿ›‚', '๐Ÿ›‚ Passport Control'), ('๐Ÿ›ƒ', '๐Ÿ›ƒ Customs'), ('๐Ÿ›„', '๐Ÿ›„ Baggage Claim'), ('๐Ÿ›…', '๐Ÿ›… Left Luggage'), ('๐Ÿšช', '๐Ÿšช Door'), ('๐Ÿšฝ', '๐Ÿšฝ Toilet'), ('๐Ÿšฟ', '๐Ÿšฟ Shower'), ('๐Ÿ›€', '๐Ÿ›€ Bath'), ('๐Ÿ›', '๐Ÿ› Bathtub'), ('โณ', 'โณ Hourglass'), ('โฐ', 'โฐ Alarm Clock'), ('โฑ', 'โฑ Stopwatch'), ('โฒ', 'โฒ Timer Clock'), ('๐Ÿ•›', "๐Ÿ•› Twelve O'Clock"), ('๐Ÿ•ง', '๐Ÿ•ง Half Past Twelve'), ('๐Ÿ•', "๐Ÿ• One O'Clock"), ('๐Ÿ•œ', '๐Ÿ•œ Half Past One'), ('๐Ÿ•‘', "๐Ÿ•‘ Two O'Clock"), ('๐Ÿ•', '๐Ÿ• Half Past Two'), ('๐Ÿ•’', "๐Ÿ•’ Three O'Clock"), ('๐Ÿ•ž', '๐Ÿ•ž Half Past Three'), ('๐Ÿ•“', "๐Ÿ•“ Four O'Clock"), ('๐Ÿ•Ÿ', '๐Ÿ•Ÿ Half Past Four'), ('๐Ÿ•”', "๐Ÿ•” Five O'Clock"), ('๐Ÿ• ', '๐Ÿ•  Half Past Five'), ('๐Ÿ••', "๐Ÿ•• Six O'Clock"), ('๐Ÿ•ก', '๐Ÿ•ก Half Past Six'), ('๐Ÿ•–', "๐Ÿ•– Seven O'Clock"), ('๐Ÿ•ข', '๐Ÿ•ข Half Past Seven'), ('๐Ÿ•—', "๐Ÿ•— Eight O'Clock"), ('๐Ÿ•ฃ', '๐Ÿ•ฃ Half Past Eight'), ('๐Ÿ•˜', "๐Ÿ•˜ Nine O'Clock"), ('๐Ÿ•ค', '๐Ÿ•ค Half Past Nine'), ('๐Ÿ•™', "๐Ÿ•™ Ten O'Clock"), ('๐Ÿ•ฅ', '๐Ÿ•ฅ Half Past Ten'), ('๐Ÿ•š', "๐Ÿ•š Eleven O'Clock"), ('๐Ÿ•ฆ', '๐Ÿ•ฆ Half Past Eleven'), ('โ›Ž', 'โ›Ž Ophiuchus'), ('๐ŸŒ‘', '๐ŸŒ‘ New Moon'), ('๐ŸŒ’', '๐ŸŒ’ Waxing Crescent'), ('๐ŸŒ“', '๐ŸŒ“ First Quarter Moon Symbol'), ('๐ŸŒ”', '๐ŸŒ” Waxing Gibbous'), ('๐ŸŒ•', '๐ŸŒ• Full Moon'), ('๐ŸŒ–', '๐ŸŒ– Waning Gibbous'), ('๐ŸŒ—', '๐ŸŒ— Half Moon'), ('๐ŸŒ˜', '๐ŸŒ˜ Waning Crescent'), ('๐ŸŒ™', '๐ŸŒ™ Crescent Moon'), ('๐ŸŒš', '๐ŸŒš New Moon With Face'), ('๐ŸŒ›', '๐ŸŒ› First Quarter Moon With Face'), ('๐ŸŒœ', '๐ŸŒœ Last Quarter Moon With Face'), ('๐ŸŒ', '๐ŸŒ Full Moon With Face'), ('๐ŸŒž', '๐ŸŒž Sun'), ('๐ŸŒ€', '๐ŸŒ€ Cyclone'), ('๐ŸŒˆ', '๐ŸŒˆ Rainbow'), ('๐ŸŒ‚', '๐ŸŒ‚ Umbrella'), ('๐ŸŒŸ', '๐ŸŒŸ Glowing Star'), ('๐ŸŒ ', '๐ŸŒ  Shooting Star'), ('๐Ÿ”ฅ', '๐Ÿ”ฅ Fire'), ('๐ŸŽƒ', '๐ŸŽƒ Jack-O-Lantern'), ('๐ŸŽ„', '๐ŸŽ„ Presents Tree'), ('๐ŸŽ†', '๐ŸŽ† Fireworks'), ('๐ŸŽ‡', '๐ŸŽ‡ Firework Sparkler'), ('โœจ', 'โœจ Sparkles'), ('๐ŸŽˆ', '๐ŸŽˆ Balloon'), ('๐ŸŽ‰', '๐ŸŽ‰ Party Pop'), ('๐ŸŽŠ', '๐ŸŽŠ Confetti Ball'), ('๐ŸŽ‹', '๐ŸŽ‹ Tanabata Tree'), ('๐ŸŽŒ', '๐ŸŽŒ Crossed Flags'), ('๐ŸŽ', '๐ŸŽ Pine Decoration'), ('๐ŸŽŽ', '๐ŸŽŽ Japanese Dolls'), ('๐ŸŽ', '๐ŸŽ Carp Streamer'), ('๐ŸŽ', '๐ŸŽ Wind Chime'), ('๐ŸŽ‘', '๐ŸŽ‘ Moon Viewing Ceremony'), ('๐ŸŽ“', '๐ŸŽ“ Grad Cap'), ('๐ŸŽฏ', '๐ŸŽฏ Bullseye'), ('๐ŸŽด', '๐ŸŽด Flower Playing Cards'), ('๐ŸŽ€', '๐ŸŽ€ Ribbon'), ('๐ŸŽ', '๐ŸŽ Wrapped Present'), ('๐ŸŽซ', '๐ŸŽซ Ticket'), ('๐Ÿ€', '๐Ÿ€ Basketball'), ('๐Ÿˆ', '๐Ÿˆ America Ball'), ('๐Ÿ‰', '๐Ÿ‰ Rugby Ball'), ('๐ŸŽพ', '๐ŸŽพ Tennis'), ('๐ŸŽฑ', '๐ŸŽฑ Billiards'), ('๐ŸŽณ', '๐ŸŽณ Bowling'), ('๐ŸŽฃ', '๐ŸŽฃ Fishing Pole and Fish'), ('๐ŸŽฝ', '๐ŸŽฝ Running Shirt With Sash'), ('๐ŸŽฟ', '๐ŸŽฟ Ski and Ski Boot'), ('๐Ÿ‚', '๐Ÿ‚ Snowboarder'), ('๐Ÿ„', '๐Ÿ„ Surfer'), ('๐Ÿ‡', '๐Ÿ‡ Horse Racing'), ('๐ŸŠ', '๐ŸŠ Swimmer'), ('๐Ÿšด', '๐Ÿšด Bicyclist'), ('๐Ÿšต', '๐Ÿšต Mountain Bicyclist'), ('๐Ÿ†', '๐Ÿ† Trophy'), ('๐ŸŽฎ', '๐ŸŽฎ Video Game'), ('๐ŸŽฒ', '๐ŸŽฒ Random Cube'), ('๐Ÿƒ', '๐Ÿƒ Playing Card Black Joker'), ('๐Ÿ”‡', '๐Ÿ”‡ Speaker With Cancellation Stroke'), ('๐Ÿ”ˆ', '๐Ÿ”ˆ Speaker'), ('๐Ÿ”‰', '๐Ÿ”‰ Speaker With One Sound Wave'), ('๐Ÿ”Š', '๐Ÿ”Š Speaker With Three Sound Waves'), ('๐Ÿ“ข', '๐Ÿ“ข Public Address Loudspeaker'), ('๐Ÿ“ฃ', '๐Ÿ“ฃ Loud Phone'), ('๐Ÿ“ฏ', '๐Ÿ“ฏ Horn'), ('๐Ÿ””', '๐Ÿ”” Bell'), ('๐Ÿ”•', '๐Ÿ”• No Bells'), ('๐Ÿ”€', '๐Ÿ”€ Shuffle'), ('๐Ÿ”', '๐Ÿ” Repeat'), ('๐Ÿ”‚', '๐Ÿ”‚ Repeat Once'), ('โฉ', 'โฉ Fast Forward'), ('โญ', 'โญ Next Track'), ('โฏ', 'โฏ Play/Pause'), ('โช', 'โช Rewind'), ('โฎ', 'โฎ Previous Track'), ('๐Ÿ”ผ', '๐Ÿ”ผ Up-Pointing Small Red Triangle'), ('โซ', 'โซ Up to Top'), ('๐Ÿ”ฝ', '๐Ÿ”ฝ Down-Pointing Small Red Triangle'), ('โฌ', 'โฌ Down to Bottom'), ('๐ŸŽผ', '๐ŸŽผ Musical Score'), ('๐ŸŽต', '๐ŸŽต Musical Note'), ('๐ŸŽถ', '๐ŸŽถ Music Notes'), ('๐ŸŽค', '๐ŸŽค Microphone'), ('๐ŸŽง', '๐ŸŽง Headphone'), ('๐ŸŽท', '๐ŸŽท Saxophone'), ('๐ŸŽธ', '๐ŸŽธ Guitar'), ('๐ŸŽน', '๐ŸŽน Keyboard'), ('๐ŸŽบ', '๐ŸŽบ Trumpet'), ('๐ŸŽป', '๐ŸŽป Violin'), ('๐Ÿ“ป', '๐Ÿ“ป Boom Box'), ('๐Ÿ“ฑ', '๐Ÿ“ฑ Internet Phone'), ('๐Ÿ“ณ', '๐Ÿ“ณ Vibration Mode'), ('๐Ÿ“ด', '๐Ÿ“ด Mobile Phone Off'), ('๐Ÿ“ฒ', '๐Ÿ“ฒ Download to Phone'), ('๐Ÿ“ต', '๐Ÿ“ต No Mobile Phones'), ('๐Ÿ“ž', '๐Ÿ“ž Old Phone'), ('๐Ÿ”Ÿ', '๐Ÿ”Ÿ Keycap Ten'), ('๐Ÿ“ถ', '๐Ÿ“ถ Antenna With Bars'), ('๐Ÿ“Ÿ', '๐Ÿ“Ÿ Pager'), ('๐Ÿ“ ', '๐Ÿ“  Fax Machine'), ('๐Ÿ”‹', '๐Ÿ”‹ Battery'), ('๐Ÿ”Œ', '๐Ÿ”Œ Plug'), ('๐Ÿ’ป', '๐Ÿ’ป Personal Computer'), ('๐Ÿ’ฝ', '๐Ÿ’ฝ Minidisc'), ('๐Ÿ’พ', '๐Ÿ’พ Floppy'), ('๐Ÿ’ฟ', '๐Ÿ’ฟ Compact Disc'), ('๐Ÿ“€', '๐Ÿ“€ DVD'), ('๐ŸŽฅ', '๐ŸŽฅ Movie Camera'), ('๐ŸŽฆ', '๐ŸŽฆ Cinema'), ('๐ŸŽฌ', '๐ŸŽฌ Clapper'), ('๐Ÿ“บ', '๐Ÿ“บ Television'), ('๐Ÿ“ท', '๐Ÿ“ท Camera'), ('๐Ÿ“น', '๐Ÿ“น Video Camera'), ('๐Ÿ“ผ', '๐Ÿ“ผ Videocassette'), ('๐Ÿ”…', '๐Ÿ”… Low Brightness Symbol'), ('๐Ÿ”†', '๐Ÿ”† High Brightness Symbol'), ('๐Ÿ”', '๐Ÿ” Bigger Glass'), ('๐Ÿ”Ž', '๐Ÿ”Ž Right-Pointing Magnifying Glass'), ('๐Ÿ”ฌ', '๐Ÿ”ฌ Microscope'), ('๐Ÿ”ญ', '๐Ÿ”ญ Telescope'), ('๐Ÿ“ก', '๐Ÿ“ก Satellite Dish'), ('๐Ÿ’ก', '๐Ÿ’ก Light Bulb'), ('๐Ÿ”ฆ', '๐Ÿ”ฆ Electric Torch'), ('๐Ÿฎ', '๐Ÿฎ Izakaya Lantern'), ('๐Ÿ“”', '๐Ÿ“” Notebook With Decorative Cover'), ('๐Ÿ“•', '๐Ÿ“• Closed Book'), ('๐Ÿ“–', '๐Ÿ“– Open Book'), ('๐Ÿ“—', '๐Ÿ“— Green Book'), ('๐Ÿ“˜', '๐Ÿ“˜ Blue Book'), ('๐Ÿ“™', '๐Ÿ“™ Orange Book'), ('๐Ÿ“š', '๐Ÿ“š Books'), ('๐Ÿ““', '๐Ÿ““ Notebook'), ('๐Ÿ“’', '๐Ÿ“’ Ledger'), ('๐Ÿ“ƒ', '๐Ÿ“ƒ Page With Curl'), ('๐Ÿ“œ', '๐Ÿ“œ Scroll'), ('๐Ÿ“„', '๐Ÿ“„ Page Facing Up'), ('๐Ÿ“ฐ', '๐Ÿ“ฐ Newspaper'), ('๐Ÿ“‘', '๐Ÿ“‘ Bookmark Tabs'), ('๐Ÿ”–', '๐Ÿ”– Bookmark'), ('๐Ÿ’ฐ', '๐Ÿ’ฐ Money Bag'), ('๐Ÿ’ด', '๐Ÿ’ด Banknote With Yen Sign'), ('๐Ÿ’ต', '๐Ÿ’ต Banknote With Dollar Sign'), ('๐Ÿ’ถ', '๐Ÿ’ถ Banknote With Euro Sign'), ('๐Ÿ’ท', '๐Ÿ’ท Banknote With Pound Sign'), ('๐Ÿ’ธ', '๐Ÿ’ธ Flying Money'), ('๐Ÿ’ฑ', '๐Ÿ’ฑ Currency Exchange'), ('๐Ÿ’ฒ', '๐Ÿ’ฒ Heavy Dollar Sign'), ('๐Ÿ’ณ', '๐Ÿ’ณ Credit Card'), ('๐Ÿ’น', '๐Ÿ’น Upwards Trend in Yen'), ('๐Ÿ“ง', '๐Ÿ“ง E-Mail Symbol'), ('๐Ÿ“จ', '๐Ÿ“จ Incoming Envelope'), ('๐Ÿ“ฉ', '๐Ÿ“ฉ Going Into Envelope'), ('๐Ÿ“ค', '๐Ÿ“ค Outbox Tray'), ('๐Ÿ“ฅ', '๐Ÿ“ฅ Inbox Tray'), ('๐Ÿ“ฆ', '๐Ÿ“ฆ Package'), ('๐Ÿ“ซ', '๐Ÿ“ซ Mailbox'), ('๐Ÿ“ช', '๐Ÿ“ช Closed Mailbox With Lowered Flag'), ('๐Ÿ“ฌ', '๐Ÿ“ฌ Open Mailbox With Raised Flag'), ('๐Ÿ“ญ', '๐Ÿ“ญ Open Mailbox With Lowered Flag'), ('๐Ÿ“ฎ', '๐Ÿ“ฎ Postbox'), ('๐Ÿ“', '๐Ÿ“ Memo'), ('๐Ÿ’ผ', '๐Ÿ’ผ Briefcase'), ('๐Ÿ“', '๐Ÿ“ File Folder'), ('๐Ÿ“‚', '๐Ÿ“‚ Open File Folder'), ('๐Ÿ“…', '๐Ÿ“… Dated'), ('๐Ÿ“†', '๐Ÿ“† Tear-Off Calendar'), ('๐Ÿ“‡', '๐Ÿ“‡ Card Index'), ('๐Ÿ“ˆ', '๐Ÿ“ˆ Up Trend'), ('๐Ÿ“‰', '๐Ÿ“‰ Down Trend'), ('๐Ÿ“Š', '๐Ÿ“Š Bar Chart'), ('๐Ÿ“‹', '๐Ÿ“‹ Clipboard'), ('๐Ÿ“Œ', '๐Ÿ“Œ Pushpin'), ('๐Ÿ“', '๐Ÿ“ Location'), ('๐Ÿ“Ž', '๐Ÿ“Ž Paperclip'), ('๐Ÿ“', '๐Ÿ“ Straight Line'), ('๐Ÿ“', '๐Ÿ“ Three Sides'), ('๐Ÿ“›', '๐Ÿ“› Name Badge'), ('๐Ÿ”’', '๐Ÿ”’ Lock'), ('๐Ÿ”“', '๐Ÿ”“ Open Lock'), ('๐Ÿ”', '๐Ÿ” Lock With Ink Pen'), ('๐Ÿ”', '๐Ÿ” Closed Lock With Key'), ('๐Ÿ”‘', '๐Ÿ”‘ Key'), ('๐Ÿ”จ', '๐Ÿ”จ Hammer'), ('๐Ÿ”ง', '๐Ÿ”ง Spanner'), ('๐Ÿ”ฉ', '๐Ÿ”ฉ Calipers'), ('๐Ÿ”—', '๐Ÿ”— Link Symbol'), ('๐Ÿ’‰', '๐Ÿ’‰ Syringe'), ('๐Ÿ’Š', '๐Ÿ’Š Pill'), ('๐Ÿ”ช', '๐Ÿ”ช Chef Knife'), ('๐Ÿ”ซ', '๐Ÿ”ซ Pistol'), ('๐Ÿšฌ', '๐Ÿšฌ Durry'), ('๐Ÿ', '๐Ÿ Get Set Go'), ('๐Ÿšฉ', '๐Ÿšฉ Triangular Flag on Post'), ('๐Ÿ‡ฆ๐Ÿ‡ซ', '๐Ÿ‡ฆ๐Ÿ‡ซ Afghanistan'), ('๐Ÿ‡ฆ๐Ÿ‡ฝ', '๐Ÿ‡ฆ๐Ÿ‡ฝ ร…land Islands'), ('๐Ÿ‡ฆ๐Ÿ‡ฑ', '๐Ÿ‡ฆ๐Ÿ‡ฑ Albania'), ('๐Ÿ‡ฉ๐Ÿ‡ฟ', '๐Ÿ‡ฉ๐Ÿ‡ฟ Algeria'), ('๐Ÿ‡ฆ๐Ÿ‡ธ', '๐Ÿ‡ฆ๐Ÿ‡ธ American Samoa'), ('๐Ÿ‡ฆ๐Ÿ‡ฉ', '๐Ÿ‡ฆ๐Ÿ‡ฉ Andorra'), ('๐Ÿ‡ฆ๐Ÿ‡ด', '๐Ÿ‡ฆ๐Ÿ‡ด Angola'), ('๐Ÿ‡ฆ๐Ÿ‡ฎ', '๐Ÿ‡ฆ๐Ÿ‡ฎ Anguilla'), ('๐Ÿ‡ฆ๐Ÿ‡ถ', '๐Ÿ‡ฆ๐Ÿ‡ถ Antarctica'), ('๐Ÿ‡ฆ๐Ÿ‡ฌ', '๐Ÿ‡ฆ๐Ÿ‡ฌ Antigua & Barbuda'), ('๐Ÿ‡ฆ๐Ÿ‡ท', '๐Ÿ‡ฆ๐Ÿ‡ท Argentina'), ('๐Ÿ‡ฆ๐Ÿ‡ฒ', '๐Ÿ‡ฆ๐Ÿ‡ฒ Armenia'), ('๐Ÿ‡ฆ๐Ÿ‡ผ', '๐Ÿ‡ฆ๐Ÿ‡ผ Aruba'), ('๐Ÿ‡ฆ๐Ÿ‡จ', '๐Ÿ‡ฆ๐Ÿ‡จ Ascension Island'), ('๐Ÿ‡ฆ๐Ÿ‡บ', '๐Ÿ‡ฆ๐Ÿ‡บ Australia'), ('๐Ÿ‡ฆ๐Ÿ‡น', '๐Ÿ‡ฆ๐Ÿ‡น Austria'), ('๐Ÿ‡ฆ๐Ÿ‡ฟ', '๐Ÿ‡ฆ๐Ÿ‡ฟ Azerbaijan'), ('๐Ÿ‡ง๐Ÿ‡ธ', '๐Ÿ‡ง๐Ÿ‡ธ Bahamas'), ('๐Ÿ‡ง๐Ÿ‡ญ', '๐Ÿ‡ง๐Ÿ‡ญ Bahrain'), ('๐Ÿ‡ง๐Ÿ‡ฉ', '๐Ÿ‡ง๐Ÿ‡ฉ Bangladesh'), ('๐Ÿ‡ง๐Ÿ‡ง', '๐Ÿ‡ง๐Ÿ‡ง Barbados'), ('๐Ÿ‡ง๐Ÿ‡พ', '๐Ÿ‡ง๐Ÿ‡พ Belarus'), ('๐Ÿ‡ง๐Ÿ‡ช', '๐Ÿ‡ง๐Ÿ‡ช Belgium'), ('๐Ÿ‡ง๐Ÿ‡ฟ', '๐Ÿ‡ง๐Ÿ‡ฟ Belize'), ('๐Ÿ‡ง๐Ÿ‡ฏ', '๐Ÿ‡ง๐Ÿ‡ฏ Benin'), ('๐Ÿ‡ง๐Ÿ‡ฒ', '๐Ÿ‡ง๐Ÿ‡ฒ Bermuda'), ('๐Ÿ‡ง๐Ÿ‡น', '๐Ÿ‡ง๐Ÿ‡น Bhutan'), ('๐Ÿ‡ง๐Ÿ‡ด', '๐Ÿ‡ง๐Ÿ‡ด Bolivia'), ('๐Ÿ‡ง๐Ÿ‡ฆ', '๐Ÿ‡ง๐Ÿ‡ฆ Bosnia & Herzegovina'), ('๐Ÿ‡ง๐Ÿ‡ผ', '๐Ÿ‡ง๐Ÿ‡ผ Botswana'), ('๐Ÿ‡ง๐Ÿ‡ป', '๐Ÿ‡ง๐Ÿ‡ป Bouvet Island'), ('๐Ÿ‡ง๐Ÿ‡ท', '๐Ÿ‡ง๐Ÿ‡ท Brazil'), ('๐Ÿ‡ฎ๐Ÿ‡ด', '๐Ÿ‡ฎ๐Ÿ‡ด British Indian Ocean Territory'), ('๐Ÿ‡ป๐Ÿ‡ฌ', '๐Ÿ‡ป๐Ÿ‡ฌ British Virgin Islands'), ('๐Ÿ‡ง๐Ÿ‡ณ', '๐Ÿ‡ง๐Ÿ‡ณ Brunei'), ('๐Ÿ‡ง๐Ÿ‡ฌ', '๐Ÿ‡ง๐Ÿ‡ฌ Bulgaria'), ('๐Ÿ‡ง๐Ÿ‡ซ', '๐Ÿ‡ง๐Ÿ‡ซ Burkina Faso'), ('๐Ÿ‡ง๐Ÿ‡ฎ', '๐Ÿ‡ง๐Ÿ‡ฎ Burundi'), ('๐Ÿ‡ฐ๐Ÿ‡ญ', '๐Ÿ‡ฐ๐Ÿ‡ญ Cambodia'), ('๐Ÿ‡จ๐Ÿ‡ฒ', '๐Ÿ‡จ๐Ÿ‡ฒ Cameroon'), ('๐Ÿ‡จ๐Ÿ‡ฆ', '๐Ÿ‡จ๐Ÿ‡ฆ Canada'), ('๐Ÿ‡ฎ๐Ÿ‡จ', '๐Ÿ‡ฎ๐Ÿ‡จ Canary Islands'), ('๐Ÿ‡จ๐Ÿ‡ป', '๐Ÿ‡จ๐Ÿ‡ป Cape Verde'), ('๐Ÿ‡ง๐Ÿ‡ถ', '๐Ÿ‡ง๐Ÿ‡ถ Caribbean Netherlands'), ('๐Ÿ‡ฐ๐Ÿ‡พ', '๐Ÿ‡ฐ๐Ÿ‡พ Cayman Islands'), ('๐Ÿ‡จ๐Ÿ‡ซ', '๐Ÿ‡จ๐Ÿ‡ซ Central African Republic'), ('๐Ÿ‡ช๐Ÿ‡ฆ', '๐Ÿ‡ช๐Ÿ‡ฆ Ceuta & Melilla'), ('๐Ÿ‡น๐Ÿ‡ฉ', '๐Ÿ‡น๐Ÿ‡ฉ Chad'), ('๐Ÿ‡จ๐Ÿ‡ฑ', '๐Ÿ‡จ๐Ÿ‡ฑ Chile'), ('๐Ÿ‡จ๐Ÿ‡ณ', '๐Ÿ‡จ๐Ÿ‡ณ China'), ('๐Ÿ‡จ๐Ÿ‡ฝ', '๐Ÿ‡จ๐Ÿ‡ฝ Christmas Island'), ('๐Ÿ‡จ๐Ÿ‡ต', '๐Ÿ‡จ๐Ÿ‡ต Clipperton Island'), ('๐Ÿ‡จ๐Ÿ‡จ', '๐Ÿ‡จ๐Ÿ‡จ Cocos Islands'), ('๐Ÿ‡จ๐Ÿ‡ด', '๐Ÿ‡จ๐Ÿ‡ด Colombia'), ('๐Ÿ‡ฐ๐Ÿ‡ฒ', '๐Ÿ‡ฐ๐Ÿ‡ฒ Comoros'), ('๐Ÿ‡จ๐Ÿ‡ฌ', '๐Ÿ‡จ๐Ÿ‡ฌ Congo - Brazzaville'), ('๐Ÿ‡จ๐Ÿ‡ฉ', '๐Ÿ‡จ๐Ÿ‡ฉ Congo - Kinshasa'), ('๐Ÿ‡จ๐Ÿ‡ฐ', '๐Ÿ‡จ๐Ÿ‡ฐ Cook Islands'), ('๐Ÿ‡จ๐Ÿ‡ท', '๐Ÿ‡จ๐Ÿ‡ท Costa Rica'), ('๐Ÿ‡จ๐Ÿ‡ฎ', '๐Ÿ‡จ๐Ÿ‡ฎ Cรดte Dโ€™Ivoire'), ('๐Ÿ‡ญ๐Ÿ‡ท', '๐Ÿ‡ญ๐Ÿ‡ท Croatia'), ('๐Ÿ‡จ๐Ÿ‡บ', '๐Ÿ‡จ๐Ÿ‡บ Cuba'), ('๐Ÿ‡จ๐Ÿ‡ผ', '๐Ÿ‡จ๐Ÿ‡ผ Curaรงao'), ('๐Ÿ‡จ๐Ÿ‡พ', '๐Ÿ‡จ๐Ÿ‡พ Cyprus'), ('๐Ÿ‡จ๐Ÿ‡ฟ', '๐Ÿ‡จ๐Ÿ‡ฟ Czech Republic'), ('๐Ÿ‡ฉ๐Ÿ‡ฐ', '๐Ÿ‡ฉ๐Ÿ‡ฐ Denmark'), ('๐Ÿ‡ฉ๐Ÿ‡ฌ', '๐Ÿ‡ฉ๐Ÿ‡ฌ Diego Garcia'), ('๐Ÿ‡ฉ๐Ÿ‡ฏ', '๐Ÿ‡ฉ๐Ÿ‡ฏ Djibouti'), ('๐Ÿ‡ฉ๐Ÿ‡ฒ', '๐Ÿ‡ฉ๐Ÿ‡ฒ Dominica'), ('๐Ÿ‡ฉ๐Ÿ‡ด', '๐Ÿ‡ฉ๐Ÿ‡ด Dominican Republic'), ('๐Ÿ‡ช๐Ÿ‡จ', '๐Ÿ‡ช๐Ÿ‡จ Ecuador'), ('๐Ÿ‡ช๐Ÿ‡ฌ', '๐Ÿ‡ช๐Ÿ‡ฌ Egypt'), ('๐Ÿ‡ธ๐Ÿ‡ป', '๐Ÿ‡ธ๐Ÿ‡ป El Salvador'), ('๐Ÿ‡ฌ๐Ÿ‡ถ', '๐Ÿ‡ฌ๐Ÿ‡ถ Equatorial Guinea'), ('๐Ÿ‡ช๐Ÿ‡ท', '๐Ÿ‡ช๐Ÿ‡ท Eritrea'), ('๐Ÿ‡ช๐Ÿ‡ช', '๐Ÿ‡ช๐Ÿ‡ช Estonia'), ('๐Ÿ‡ช๐Ÿ‡น', '๐Ÿ‡ช๐Ÿ‡น Ethiopia'), ('๐Ÿ‡ช๐Ÿ‡บ', '๐Ÿ‡ช๐Ÿ‡บ European Union'), ('๐Ÿ‡ซ๐Ÿ‡ฐ', '๐Ÿ‡ซ๐Ÿ‡ฐ Falkland Islands'), ('๐Ÿ‡ซ๐Ÿ‡ด', '๐Ÿ‡ซ๐Ÿ‡ด Faroe Islands'), ('๐Ÿ‡ซ๐Ÿ‡ฏ', '๐Ÿ‡ซ๐Ÿ‡ฏ Fiji'), ('๐Ÿ‡ซ๐Ÿ‡ฎ', '๐Ÿ‡ซ๐Ÿ‡ฎ Finland'), ('๐Ÿ‡ซ๐Ÿ‡ท', '๐Ÿ‡ซ๐Ÿ‡ท France'), ('๐Ÿ‡ฌ๐Ÿ‡ซ', '๐Ÿ‡ฌ๐Ÿ‡ซ French Guiana'), ('๐Ÿ‡ต๐Ÿ‡ซ', '๐Ÿ‡ต๐Ÿ‡ซ French Polynesia'), ('๐Ÿ‡น๐Ÿ‡ซ', '๐Ÿ‡น๐Ÿ‡ซ French Southern Territories'), ('๐Ÿ‡ฌ๐Ÿ‡ฆ', '๐Ÿ‡ฌ๐Ÿ‡ฆ Gabon'), ('๐Ÿ‡ฌ๐Ÿ‡ฒ', '๐Ÿ‡ฌ๐Ÿ‡ฒ Gambia'), ('๐Ÿ‡ฌ๐Ÿ‡ช', '๐Ÿ‡ฌ๐Ÿ‡ช Georgia'), ('๐Ÿ‡ฉ๐Ÿ‡ช', '๐Ÿ‡ฉ๐Ÿ‡ช Germany'), ('๐Ÿ‡ฌ๐Ÿ‡ญ', '๐Ÿ‡ฌ๐Ÿ‡ญ Ghana'), ('๐Ÿ‡ฌ๐Ÿ‡ฎ', '๐Ÿ‡ฌ๐Ÿ‡ฎ Gibraltar'), ('๐Ÿ‡ฌ๐Ÿ‡ท', '๐Ÿ‡ฌ๐Ÿ‡ท Greece'), ('๐Ÿ‡ฌ๐Ÿ‡ฑ', '๐Ÿ‡ฌ๐Ÿ‡ฑ Greenland'), ('๐Ÿ‡ฌ๐Ÿ‡ฉ', '๐Ÿ‡ฌ๐Ÿ‡ฉ Grenada'), ('๐Ÿ‡ฌ๐Ÿ‡ต', '๐Ÿ‡ฌ๐Ÿ‡ต Guadeloupe'), ('๐Ÿ‡ฌ๐Ÿ‡บ', '๐Ÿ‡ฌ๐Ÿ‡บ Guam'), ('๐Ÿ‡ฌ๐Ÿ‡น', '๐Ÿ‡ฌ๐Ÿ‡น Guatemala'), ('๐Ÿ‡ฌ๐Ÿ‡ฌ', '๐Ÿ‡ฌ๐Ÿ‡ฌ Guernsey'), ('๐Ÿ‡ฌ๐Ÿ‡ณ', '๐Ÿ‡ฌ๐Ÿ‡ณ Guinea'), ('๐Ÿ‡ฌ๐Ÿ‡ผ', '๐Ÿ‡ฌ๐Ÿ‡ผ Guinea-Bissau'), ('๐Ÿ‡ฌ๐Ÿ‡พ', '๐Ÿ‡ฌ๐Ÿ‡พ Guyana'), ('๐Ÿ‡ญ๐Ÿ‡น', '๐Ÿ‡ญ๐Ÿ‡น Haiti'), ('๐Ÿ‡ญ๐Ÿ‡ฒ', '๐Ÿ‡ญ๐Ÿ‡ฒ Heard & McDonald Islands'), ('๐Ÿ‡ญ๐Ÿ‡ณ', '๐Ÿ‡ญ๐Ÿ‡ณ Honduras'), ('๐Ÿ‡ญ๐Ÿ‡ฐ', '๐Ÿ‡ญ๐Ÿ‡ฐ Hong Kong'), ('๐Ÿ‡ญ๐Ÿ‡บ', '๐Ÿ‡ญ๐Ÿ‡บ Hungary'), ('๐Ÿ‡ฎ๐Ÿ‡ธ', '๐Ÿ‡ฎ๐Ÿ‡ธ Iceland'), ('๐Ÿ‡ฎ๐Ÿ‡ณ', '๐Ÿ‡ฎ๐Ÿ‡ณ India'), ('๐Ÿ‡ฎ๐Ÿ‡ฉ', '๐Ÿ‡ฎ๐Ÿ‡ฉ Indonesia'), ('๐Ÿ‡ฎ๐Ÿ‡ท', '๐Ÿ‡ฎ๐Ÿ‡ท Iran'), ('๐Ÿ‡ฎ๐Ÿ‡ถ', '๐Ÿ‡ฎ๐Ÿ‡ถ Iraq'), ('๐Ÿ‡ฎ๐Ÿ‡ช', '๐Ÿ‡ฎ๐Ÿ‡ช Ireland'), ('๐Ÿ‡ฎ๐Ÿ‡ฒ', '๐Ÿ‡ฎ๐Ÿ‡ฒ Isle of Man'), ('๐Ÿ‡ฎ๐Ÿ‡ฑ', '๐Ÿ‡ฎ๐Ÿ‡ฑ Israel'), ('๐Ÿ‡ฎ๐Ÿ‡น', '๐Ÿ‡ฎ๐Ÿ‡น Italy'), ('๐Ÿ‡ฏ๐Ÿ‡ฒ', '๐Ÿ‡ฏ๐Ÿ‡ฒ Jamaica'), ('๐Ÿ‡ฏ๐Ÿ‡ต', '๐Ÿ‡ฏ๐Ÿ‡ต Japan'), ('๐Ÿ‡ฏ๐Ÿ‡ช', '๐Ÿ‡ฏ๐Ÿ‡ช Jersey'), ('๐Ÿ‡ฏ๐Ÿ‡ด', '๐Ÿ‡ฏ๐Ÿ‡ด Jordan'), ('๐Ÿ‡ฐ๐Ÿ‡ฟ', '๐Ÿ‡ฐ๐Ÿ‡ฟ Kazakhstan'), ('๐Ÿ‡ฐ๐Ÿ‡ช', '๐Ÿ‡ฐ๐Ÿ‡ช Kenya'), ('๐Ÿ‡ฐ๐Ÿ‡ฎ', '๐Ÿ‡ฐ๐Ÿ‡ฎ Kiribati'), ('๐Ÿ‡ฝ๐Ÿ‡ฐ', '๐Ÿ‡ฝ๐Ÿ‡ฐ Kosovo'), ('๐Ÿ‡ฐ๐Ÿ‡ผ', '๐Ÿ‡ฐ๐Ÿ‡ผ Kuwait'), ('๐Ÿ‡ฐ๐Ÿ‡ฌ', '๐Ÿ‡ฐ๐Ÿ‡ฌ Kyrgyzstan'), ('๐Ÿ‡ฑ๐Ÿ‡ฆ', '๐Ÿ‡ฑ๐Ÿ‡ฆ Laos'), ('๐Ÿ‡ฑ๐Ÿ‡ป', '๐Ÿ‡ฑ๐Ÿ‡ป Latvia'), ('๐Ÿ‡ฑ๐Ÿ‡ง', '๐Ÿ‡ฑ๐Ÿ‡ง Lebanon'), ('๐Ÿ‡ฑ๐Ÿ‡ธ', '๐Ÿ‡ฑ๐Ÿ‡ธ Lesotho'), ('๐Ÿ‡ฑ๐Ÿ‡ท', '๐Ÿ‡ฑ๐Ÿ‡ท Liberia'), ('๐Ÿ‡ฑ๐Ÿ‡พ', '๐Ÿ‡ฑ๐Ÿ‡พ Libya'), ('๐Ÿ‡ฑ๐Ÿ‡ฎ', '๐Ÿ‡ฑ๐Ÿ‡ฎ Liechtenstein'), ('๐Ÿ‡ฑ๐Ÿ‡น', '๐Ÿ‡ฑ๐Ÿ‡น Lithuania'), ('๐Ÿ‡ฑ๐Ÿ‡บ', '๐Ÿ‡ฑ๐Ÿ‡บ Luxembourg'), ('๐Ÿ‡ฒ๐Ÿ‡ด', '๐Ÿ‡ฒ๐Ÿ‡ด Macau'), ('๐Ÿ‡ฒ๐Ÿ‡ฐ', '๐Ÿ‡ฒ๐Ÿ‡ฐ Macedonia'), ('๐Ÿ‡ฒ๐Ÿ‡ฌ', '๐Ÿ‡ฒ๐Ÿ‡ฌ Madagascar'), ('๐Ÿ‡ฒ๐Ÿ‡ผ', '๐Ÿ‡ฒ๐Ÿ‡ผ Malawi'), ('๐Ÿ‡ฒ๐Ÿ‡พ', '๐Ÿ‡ฒ๐Ÿ‡พ Malaysia'), ('๐Ÿ‡ฒ๐Ÿ‡ป', '๐Ÿ‡ฒ๐Ÿ‡ป Maldives'), ('๐Ÿ‡ฒ๐Ÿ‡ฑ', '๐Ÿ‡ฒ๐Ÿ‡ฑ Mali'), ('๐Ÿ‡ฒ๐Ÿ‡น', '๐Ÿ‡ฒ๐Ÿ‡น Malta'), ('๐Ÿ‡ฒ๐Ÿ‡ญ', '๐Ÿ‡ฒ๐Ÿ‡ญ Marshall Islands'), ('๐Ÿ‡ฒ๐Ÿ‡ถ', '๐Ÿ‡ฒ๐Ÿ‡ถ Martinique'), ('๐Ÿ‡ฒ๐Ÿ‡ท', '๐Ÿ‡ฒ๐Ÿ‡ท Mauritania'), ('๐Ÿ‡ฒ๐Ÿ‡บ', '๐Ÿ‡ฒ๐Ÿ‡บ Mauritius'), ('๐Ÿ‡พ๐Ÿ‡น', '๐Ÿ‡พ๐Ÿ‡น Mayotte'), ('๐Ÿ‡ฒ๐Ÿ‡ฝ', '๐Ÿ‡ฒ๐Ÿ‡ฝ Mexico'), ('๐Ÿ‡ซ๐Ÿ‡ฒ', '๐Ÿ‡ซ๐Ÿ‡ฒ Micronesia'), ('๐Ÿ‡ฒ๐Ÿ‡ฉ', '๐Ÿ‡ฒ๐Ÿ‡ฉ Moldova'), ('๐Ÿ‡ฒ๐Ÿ‡จ', '๐Ÿ‡ฒ๐Ÿ‡จ Monaco'), ('๐Ÿ‡ฒ๐Ÿ‡ณ', '๐Ÿ‡ฒ๐Ÿ‡ณ Mongolia'), ('๐Ÿ‡ฒ๐Ÿ‡ช', '๐Ÿ‡ฒ๐Ÿ‡ช Montenegro'), ('๐Ÿ‡ฒ๐Ÿ‡ธ', '๐Ÿ‡ฒ๐Ÿ‡ธ Montserrat'), ('๐Ÿ‡ฒ๐Ÿ‡ฆ', '๐Ÿ‡ฒ๐Ÿ‡ฆ Morocco'), ('๐Ÿ‡ฒ๐Ÿ‡ฟ', '๐Ÿ‡ฒ๐Ÿ‡ฟ Mozambique'), ('๐Ÿ‡ฒ๐Ÿ‡ฒ', '๐Ÿ‡ฒ๐Ÿ‡ฒ Myanmar'), ('๐Ÿ‡ณ๐Ÿ‡ฆ', '๐Ÿ‡ณ๐Ÿ‡ฆ Namibia'), ('๐Ÿ‡ณ๐Ÿ‡ท', '๐Ÿ‡ณ๐Ÿ‡ท Nauru'), ('๐Ÿ‡ณ๐Ÿ‡ต', '๐Ÿ‡ณ๐Ÿ‡ต Nepal'), ('๐Ÿ‡ณ๐Ÿ‡ฑ', '๐Ÿ‡ณ๐Ÿ‡ฑ Netherlands'), ('๐Ÿ‡ณ๐Ÿ‡จ', '๐Ÿ‡ณ๐Ÿ‡จ New Caledonia'), ('๐Ÿ‡ณ๐Ÿ‡ฟ', '๐Ÿ‡ณ๐Ÿ‡ฟ New Zealand'), ('๐Ÿ‡ณ๐Ÿ‡ฎ', '๐Ÿ‡ณ๐Ÿ‡ฎ Nicaragua'), ('๐Ÿ‡ณ๐Ÿ‡ช', '๐Ÿ‡ณ๐Ÿ‡ช Niger'), ('๐Ÿ‡ณ๐Ÿ‡ฌ', '๐Ÿ‡ณ๐Ÿ‡ฌ Nigeria'), ('๐Ÿ‡ณ๐Ÿ‡บ', '๐Ÿ‡ณ๐Ÿ‡บ Niue'), ('๐Ÿ‡ณ๐Ÿ‡ซ', '๐Ÿ‡ณ๐Ÿ‡ซ Norfolk Island'), ('๐Ÿ‡ฒ๐Ÿ‡ต', '๐Ÿ‡ฒ๐Ÿ‡ต Northern Mariana Islands'), ('๐Ÿ‡ฐ๐Ÿ‡ต', '๐Ÿ‡ฐ๐Ÿ‡ต North Korea'), ('๐Ÿ‡ณ๐Ÿ‡ด', '๐Ÿ‡ณ๐Ÿ‡ด Norway'), ('๐Ÿ‡ด๐Ÿ‡ฒ', '๐Ÿ‡ด๐Ÿ‡ฒ Oman'), ('๐Ÿ‡ต๐Ÿ‡ฐ', '๐Ÿ‡ต๐Ÿ‡ฐ Pakistan'), ('๐Ÿ‡ต๐Ÿ‡ผ', '๐Ÿ‡ต๐Ÿ‡ผ Palau'), ('๐Ÿ‡ต๐Ÿ‡ธ', '๐Ÿ‡ต๐Ÿ‡ธ Palestinian Territories'), ('๐Ÿ‡ต๐Ÿ‡ฆ', '๐Ÿ‡ต๐Ÿ‡ฆ Panama'), ('๐Ÿ‡ต๐Ÿ‡ฌ', '๐Ÿ‡ต๐Ÿ‡ฌ Papua New Guinea'), ('๐Ÿ‡ต๐Ÿ‡พ', '๐Ÿ‡ต๐Ÿ‡พ Paraguay'), ('๐Ÿ‡ต๐Ÿ‡ช', '๐Ÿ‡ต๐Ÿ‡ช Peru'), ('๐Ÿ‡ต๐Ÿ‡ญ', '๐Ÿ‡ต๐Ÿ‡ญ Philippines'), ('๐Ÿ‡ต๐Ÿ‡ณ', '๐Ÿ‡ต๐Ÿ‡ณ Pitcairn Islands'), ('๐Ÿ‡ต๐Ÿ‡ฑ', '๐Ÿ‡ต๐Ÿ‡ฑ Poland'), ('๐Ÿ‡ต๐Ÿ‡น', '๐Ÿ‡ต๐Ÿ‡น Portugal'), ('๐Ÿ‡ต๐Ÿ‡ท', '๐Ÿ‡ต๐Ÿ‡ท Puerto Rico'), ('๐Ÿ‡ถ๐Ÿ‡ฆ', '๐Ÿ‡ถ๐Ÿ‡ฆ Qatar'), ('๐Ÿ‡ท๐Ÿ‡ช', '๐Ÿ‡ท๐Ÿ‡ช Rรฉunion'), ('๐Ÿ‡ท๐Ÿ‡ด', '๐Ÿ‡ท๐Ÿ‡ด Romania'), ('๐Ÿ‡ท๐Ÿ‡บ', '๐Ÿ‡ท๐Ÿ‡บ Russia'), ('๐Ÿ‡ท๐Ÿ‡ผ', '๐Ÿ‡ท๐Ÿ‡ผ Rwanda'), ('๐Ÿ‡ผ๐Ÿ‡ธ', '๐Ÿ‡ผ๐Ÿ‡ธ Samoa'), ('๐Ÿ‡ธ๐Ÿ‡ฒ', '๐Ÿ‡ธ๐Ÿ‡ฒ San Marino'), ('๐Ÿ‡ธ๐Ÿ‡น', '๐Ÿ‡ธ๐Ÿ‡น Sรฃo Tomรฉ & Prรญncipe'), ('๐Ÿ‡ธ๐Ÿ‡ฆ', '๐Ÿ‡ธ๐Ÿ‡ฆ Saudi Arabia'), ('๐Ÿ‡ธ๐Ÿ‡ณ', '๐Ÿ‡ธ๐Ÿ‡ณ Senegal'), ('๐Ÿ‡ท๐Ÿ‡ธ', '๐Ÿ‡ท๐Ÿ‡ธ Serbia'), ('๐Ÿ‡ธ๐Ÿ‡จ', '๐Ÿ‡ธ๐Ÿ‡จ Seychelles'), ('๐Ÿ‡ธ๐Ÿ‡ฑ', '๐Ÿ‡ธ๐Ÿ‡ฑ Sierra Leone'), ('๐Ÿ‡ธ๐Ÿ‡ฌ', '๐Ÿ‡ธ๐Ÿ‡ฌ Singapore'), ('๐Ÿ‡ธ๐Ÿ‡ฝ', '๐Ÿ‡ธ๐Ÿ‡ฝ Sint Maarten'), ('๐Ÿ‡ธ๐Ÿ‡ฐ', '๐Ÿ‡ธ๐Ÿ‡ฐ Slovakia'), ('๐Ÿ‡ธ๐Ÿ‡ฎ', '๐Ÿ‡ธ๐Ÿ‡ฎ Slovenia'), ('๐Ÿ‡ธ๐Ÿ‡ง', '๐Ÿ‡ธ๐Ÿ‡ง Solomon Islands'), ('๐Ÿ‡ธ๐Ÿ‡ด', '๐Ÿ‡ธ๐Ÿ‡ด Somalia'), ('๐Ÿ‡ฟ๐Ÿ‡ฆ', '๐Ÿ‡ฟ๐Ÿ‡ฆ South Africa'), ('๐Ÿ‡ฌ๐Ÿ‡ธ', '๐Ÿ‡ฌ๐Ÿ‡ธ South Georgia & South Sandwich Islands'), ('๐Ÿ‡ฐ๐Ÿ‡ท', '๐Ÿ‡ฐ๐Ÿ‡ท South Korea'), ('๐Ÿ‡ธ๐Ÿ‡ธ', '๐Ÿ‡ธ๐Ÿ‡ธ South Sudan'), ('๐Ÿ‡ช๐Ÿ‡ธ', '๐Ÿ‡ช๐Ÿ‡ธ Spain'), ('๐Ÿ‡ฑ๐Ÿ‡ฐ', '๐Ÿ‡ฑ๐Ÿ‡ฐ Sri Lanka'), ('๐Ÿ‡ง๐Ÿ‡ฑ', '๐Ÿ‡ง๐Ÿ‡ฑ St. Barthรฉlemy'), ('๐Ÿ‡ธ๐Ÿ‡ญ', '๐Ÿ‡ธ๐Ÿ‡ญ St. Helena'), ('๐Ÿ‡ฐ๐Ÿ‡ณ', '๐Ÿ‡ฐ๐Ÿ‡ณ St. Kitts & Nevis'), ('๐Ÿ‡ฑ๐Ÿ‡จ', '๐Ÿ‡ฑ๐Ÿ‡จ St. Lucia'), ('๐Ÿ‡ฒ๐Ÿ‡ซ', '๐Ÿ‡ฒ๐Ÿ‡ซ St. Martin'), ('๐Ÿ‡ต๐Ÿ‡ฒ', '๐Ÿ‡ต๐Ÿ‡ฒ St. Pierre & Miquelon'), ('๐Ÿ‡ป๐Ÿ‡จ', '๐Ÿ‡ป๐Ÿ‡จ St. Vincent & Grenadines'), ('๐Ÿ‡ธ๐Ÿ‡ฉ', '๐Ÿ‡ธ๐Ÿ‡ฉ Sudan'), ('๐Ÿ‡ธ๐Ÿ‡ท', '๐Ÿ‡ธ๐Ÿ‡ท Suriname'), ('๐Ÿ‡ธ๐Ÿ‡ฏ', '๐Ÿ‡ธ๐Ÿ‡ฏ Svalbard & Jan Mayen'), ('๐Ÿ‡ธ๐Ÿ‡ฟ', '๐Ÿ‡ธ๐Ÿ‡ฟ Swaziland'), ('๐Ÿ‡ธ๐Ÿ‡ช', '๐Ÿ‡ธ๐Ÿ‡ช Sweden'), ('๐Ÿ‡จ๐Ÿ‡ญ', '๐Ÿ‡จ๐Ÿ‡ญ Switzerland'), ('๐Ÿ‡ธ๐Ÿ‡พ', '๐Ÿ‡ธ๐Ÿ‡พ Syria'), ('๐Ÿ‡น๐Ÿ‡ผ', '๐Ÿ‡น๐Ÿ‡ผ Taiwan'), ('๐Ÿ‡น๐Ÿ‡ฏ', '๐Ÿ‡น๐Ÿ‡ฏ Tajikistan'), ('๐Ÿ‡น๐Ÿ‡ฟ', '๐Ÿ‡น๐Ÿ‡ฟ Tanzania'), ('๐Ÿ‡น๐Ÿ‡ญ', '๐Ÿ‡น๐Ÿ‡ญ Thailand'), ('๐Ÿ‡น๐Ÿ‡ฑ', '๐Ÿ‡น๐Ÿ‡ฑ Timor-Leste'), ('๐Ÿ‡น๐Ÿ‡ฌ', '๐Ÿ‡น๐Ÿ‡ฌ Togo'), ('๐Ÿ‡น๐Ÿ‡ฐ', '๐Ÿ‡น๐Ÿ‡ฐ Tokelau'), ('๐Ÿ‡น๐Ÿ‡ด', '๐Ÿ‡น๐Ÿ‡ด Tonga'), ('๐Ÿ‡น๐Ÿ‡น', '๐Ÿ‡น๐Ÿ‡น Trinidad & Tobago'), ('๐Ÿ‡น๐Ÿ‡ฆ', '๐Ÿ‡น๐Ÿ‡ฆ Tristan Da Cunha'), ('๐Ÿ‡น๐Ÿ‡ณ', '๐Ÿ‡น๐Ÿ‡ณ Tunisia'), ('๐Ÿ‡น๐Ÿ‡ท', '๐Ÿ‡น๐Ÿ‡ท Turkey'), ('๐Ÿ‡น๐Ÿ‡ฒ', '๐Ÿ‡น๐Ÿ‡ฒ Turkmenistan'), ('๐Ÿ‡น๐Ÿ‡จ', '๐Ÿ‡น๐Ÿ‡จ Turks & Caicos Islands'), ('๐Ÿ‡น๐Ÿ‡ป', '๐Ÿ‡น๐Ÿ‡ป Tuvalu'), ('๐Ÿ‡บ๐Ÿ‡ฌ', '๐Ÿ‡บ๐Ÿ‡ฌ Uganda'), ('๐Ÿ‡บ๐Ÿ‡ฆ', '๐Ÿ‡บ๐Ÿ‡ฆ Ukraine'), ('๐Ÿ‡ฆ๐Ÿ‡ช', '๐Ÿ‡ฆ๐Ÿ‡ช United Arab Emirates'), ('๐Ÿ‡ฌ๐Ÿ‡ง', '๐Ÿ‡ฌ๐Ÿ‡ง United Kingdom'), ('๐Ÿ‡บ๐Ÿ‡ธ', '๐Ÿ‡บ๐Ÿ‡ธ United States'), ('๐Ÿ‡บ๐Ÿ‡พ', '๐Ÿ‡บ๐Ÿ‡พ Uruguay'), ('๐Ÿ‡บ๐Ÿ‡ฒ', '๐Ÿ‡บ๐Ÿ‡ฒ U.S. Outlying Islands'), ('๐Ÿ‡ป๐Ÿ‡ฎ', '๐Ÿ‡ป๐Ÿ‡ฎ U.S. Virgin Islands'), ('๐Ÿ‡บ๐Ÿ‡ฟ', '๐Ÿ‡บ๐Ÿ‡ฟ Uzbekistan'), ('๐Ÿ‡ป๐Ÿ‡บ', '๐Ÿ‡ป๐Ÿ‡บ Vanuatu'), ('๐Ÿ‡ป๐Ÿ‡ฆ', '๐Ÿ‡ป๐Ÿ‡ฆ Vatican City'), ('๐Ÿ‡ป๐Ÿ‡ช', '๐Ÿ‡ป๐Ÿ‡ช Venezuela'), ('๐Ÿ‡ป๐Ÿ‡ณ', '๐Ÿ‡ป๐Ÿ‡ณ Vietnam'), ('๐Ÿ‡ผ๐Ÿ‡ซ', '๐Ÿ‡ผ๐Ÿ‡ซ Wallis & Futuna'), ('๐Ÿ‡ช๐Ÿ‡ญ', '๐Ÿ‡ช๐Ÿ‡ญ Western Sahara'), ('๐Ÿ‡พ๐Ÿ‡ช', '๐Ÿ‡พ๐Ÿ‡ช Yemen'), ('๐Ÿ‡ฟ๐Ÿ‡ฒ', '๐Ÿ‡ฟ๐Ÿ‡ฒ Zambia'), ('๐Ÿ‡ฟ๐Ÿ‡ผ', '๐Ÿ‡ฟ๐Ÿ‡ผ Zimbabwe'), ('๐Ÿ”ƒ', '๐Ÿ”ƒ Clockwise Arrows'), ('๐Ÿ”„', '๐Ÿ”„ Anticlockwise Arrows'), ('๐Ÿ”™', '๐Ÿ”™ Back'), ('๐Ÿ”š', '๐Ÿ”š End'), ('๐Ÿ”›', '๐Ÿ”› On'), ('๐Ÿ”œ', '๐Ÿ”œ Soon'), ('๐Ÿ”', '๐Ÿ” Top'), ('๐Ÿ”ฐ', '๐Ÿ”ฐ Beginner'), ('๐Ÿ”ฎ', '๐Ÿ”ฎ Crystal Ball'), ('๐Ÿ”ฏ', '๐Ÿ”ฏ Six Pointed Star With Middle Dot'), ('โœ…', 'โœ… White Heavy Check Mark'), ('โŒ', 'โŒ Cross'), ('โŽ', 'โŽ Negative Squared Cross Mark'), ('โž•', 'โž• Heavy Plus Sign'), ('โž–', 'โž– Heavy Minus Sign'), ('โž—', 'โž— Heavy Division Sign'), ('โžฐ', 'โžฐ Curly Loop'), ('โžฟ', 'โžฟ Double Curly Loop'), ('โ“', 'โ“ Question'), ('โ”', 'โ” White Question Mark Ornament'), ('โ•', 'โ• White Exclamation Mark Ornament'), ('๐Ÿ’ฏ', '๐Ÿ’ฏ Hundred Points'), ('๐Ÿ”ž', '๐Ÿ”ž Over Eighteen'), ('๐Ÿ” ', '๐Ÿ”  Latin Capital Letters'), ('๐Ÿ”ก', '๐Ÿ”ก Latin Small Letters'), ('๐Ÿ”ข', '๐Ÿ”ข Numbers'), ('๐Ÿ”ฃ', '๐Ÿ”ฃ Symbols'), ('๐Ÿ”ค', '๐Ÿ”ค Latin Letters'), ('๐Ÿ…ฐ๏ธ', '๐Ÿ…ฐ๏ธ Squared A'), ('๐Ÿ†Ž', '๐Ÿ†Ž Squared AB'), ('๐Ÿ…ฑ๏ธ', '๐Ÿ…ฑ๏ธ Squared B'), ('๐Ÿ†‘', '๐Ÿ†‘ Squared CL'), ('๐Ÿ†’', '๐Ÿ†’ Cool Square'), ('๐Ÿ†“', '๐Ÿ†“ Squared Free'), ('๐Ÿ†”', '๐Ÿ†” Squared ID'), ('๐Ÿ†•', '๐Ÿ†• New Square'), ('๐Ÿ†–', '๐Ÿ†– Squared NG'), ('๐Ÿ…พ๏ธ', '๐Ÿ…พ๏ธ Squared O'), ('๐Ÿ†—', '๐Ÿ†— OK Square'), ('๐Ÿ†˜', '๐Ÿ†˜ SOS Square'), ('๐Ÿ†™', '๐Ÿ†™ Squared Up!'), ('๐Ÿ†š', '๐Ÿ†š Squared Vs'), ('๐Ÿˆ', '๐Ÿˆ Squared Katakana Koko'), ('๐Ÿˆ‚๏ธ', '๐Ÿˆ‚๏ธ Squared Katakana Sa'), ('๐Ÿˆท๏ธ', '๐Ÿˆท๏ธ Squared ๆœˆ (Moon)'), ('๐Ÿˆถ', '๐Ÿˆถ Squared ๆœ‰ (Have)'), ('๐Ÿ‰', '๐Ÿ‰ Circled Ideograph Advantage'), ('๐Ÿˆน', '๐Ÿˆน Squared CJK Unified Ideograph-5272'), ('๐Ÿˆฒ', '๐Ÿˆฒ Squared CJK Unified Ideograph-7981'), ('๐Ÿ‰‘', '๐Ÿ‰‘ Circled ๅฏ (Accept)'), ('๐Ÿˆธ', '๐Ÿˆธ Squared CJK Unified Ideograph-7533'), ('๐Ÿˆด', '๐Ÿˆด Squared CJK Unified Ideograph-5408'), ('๐Ÿˆณ', '๐Ÿˆณ Squared CJK Unified Ideograph-7a7a'), ('๐Ÿˆบ', '๐Ÿˆบ Squared CJK Unified Ideograph-55b6'), ('๐Ÿˆต', '๐Ÿˆต Squared CJK Unified Ideograph-6e80'), ('๐Ÿ”ถ', '๐Ÿ”ถ Large Orange Diamond'), ('๐Ÿ”ท', '๐Ÿ”ท Large Blue Diamond'), ('๐Ÿ”ธ', '๐Ÿ”ธ Small Orange Diamond'), ('๐Ÿ”น', '๐Ÿ”น Small Blue Diamond'), ('๐Ÿ”บ', '๐Ÿ”บ Up-Pointing Red Triangle'), ('๐Ÿ”ป', '๐Ÿ”ป Down-Pointing Red Triangle'), ('๐Ÿ’ ', '๐Ÿ’  Diamond Shape With a Dot Inside'), ('๐Ÿ”˜', '๐Ÿ”˜ Radio Button'), ('๐Ÿ”ฒ', '๐Ÿ”ฒ Black Square Button'), ('๐Ÿ”ณ', '๐Ÿ”ณ White Square Button'), ('๐Ÿ”ด', '๐Ÿ”ด Large Red Circle'), ('๐Ÿ”ต', '๐Ÿ”ต Large Blue Circle'), ('๐Ÿ˜€', '๐Ÿ˜€ Grinning'), ('๐Ÿ˜—', '๐Ÿ˜— Kissing'), ('๐Ÿ˜™', '๐Ÿ˜™ Smooch'), ('๐Ÿ˜‘', '๐Ÿ˜‘ True Neutral'), ('๐Ÿ˜ฎ', '๐Ÿ˜ฎ Stunned'), ('๐Ÿ˜ฏ', '๐Ÿ˜ฏ Hushed'), ('๐Ÿ˜ด', '๐Ÿ˜ด Sleepy'), ('๐Ÿ˜›', '๐Ÿ˜› Tongue'), ('๐Ÿ˜•', '๐Ÿ˜• Confused'), ('๐Ÿ˜Ÿ', '๐Ÿ˜Ÿ Worried'), ('๐Ÿ˜ฆ', '๐Ÿ˜ฆ Frowning Face With Open Mouth'), ('๐Ÿ˜ง', '๐Ÿ˜ง Anguish Face'), ('๐Ÿ˜ฌ', '๐Ÿ˜ฌ Grimace'), ('๐Ÿ™‚', '๐Ÿ™‚ Slightly Smiling'), ('๐Ÿ™', '๐Ÿ™ Slightly Frowning'), ('๐Ÿ•ต', '๐Ÿ•ต Spy'), ('๐Ÿ—ฃ', '๐Ÿ—ฃ Speaking Head in Silhouette'), ('๐Ÿ•ด', '๐Ÿ•ด Man in Business Suit Levitating'), ('๐Ÿ–•', '๐Ÿ–• Middle Finger'), ('๐Ÿ––', '๐Ÿ–– Vulcan Hand'), ('๐Ÿ–', '๐Ÿ– Raised Hand With Fingers Splayed'), ('๐Ÿ‘', '๐Ÿ‘ Eye'), ('๐Ÿ•ณ', '๐Ÿ•ณ Hole'), ('๐Ÿ—ฏ', '๐Ÿ—ฏ Right Anger Bubble'), ('๐Ÿ•ถ', '๐Ÿ•ถ Sunglasses'), ('๐Ÿ›', '๐Ÿ› Shopping'), ('๐Ÿฟ', '๐Ÿฟ Chipmunk'), ('๐Ÿ•Š', '๐Ÿ•Š Peace Dove'), ('๐Ÿ•ท', '๐Ÿ•ท Spider'), ('๐Ÿ•ธ', '๐Ÿ•ธ Spider Web'), ('๐Ÿต', '๐Ÿต Rosette'), ('๐ŸŒถ', '๐ŸŒถ Chilli'), ('๐Ÿฝ', '๐Ÿฝ Fork and Knife With Plate'), ('๐Ÿ—บ', '๐Ÿ—บ World Map'), ('๐Ÿ”', '๐Ÿ” Snow Capped Mountain'), ('๐Ÿ•', '๐Ÿ• Camping'), ('๐Ÿ–', '๐Ÿ– Beach'), ('๐Ÿœ', '๐Ÿœ Desert'), ('๐Ÿ', '๐Ÿ Desert Island'), ('๐Ÿž', '๐Ÿž National Park'), ('๐ŸŸ', '๐ŸŸ Stadium'), ('๐Ÿ›', '๐Ÿ› Architecture'), ('๐Ÿ—', '๐Ÿ— Building Construction'), ('๐Ÿ˜', '๐Ÿ˜ House Buildings'), ('๐Ÿ™', '๐Ÿ™ Cityscape'), ('๐Ÿš', '๐Ÿš Derelict House Building'), ('๐Ÿ–ผ', '๐Ÿ–ผ Frame With Picture'), ('๐Ÿ›ข', '๐Ÿ›ข Oil Drum'), ('๐Ÿ›ฃ', '๐Ÿ›ฃ Motorway'), ('๐Ÿ›ค', '๐Ÿ›ค Railway Track'), ('๐Ÿ›ณ', '๐Ÿ›ณ Passenger Ship'), ('๐Ÿ›ฅ', '๐Ÿ›ฅ Boat'), ('๐Ÿ›ฉ', '๐Ÿ›ฉ Airplane'), ('๐Ÿ›ซ', '๐Ÿ›ซ Airplane Departure'), ('๐Ÿ›ฌ', '๐Ÿ›ฌ Airplane Arriving'), ('๐Ÿ›ฐ', '๐Ÿ›ฐ Satellite'), ('๐Ÿ›Ž', '๐Ÿ›Ž Service Bell'), ('๐Ÿ›Œ', '๐Ÿ›Œ Bed'), ('๐Ÿ›', '๐Ÿ› Bed'), ('๐Ÿ›‹', '๐Ÿ›‹ Couch and Lamp'), ('๐Ÿ•ฐ', '๐Ÿ•ฐ Mantelpiece'), ('๐ŸŒก', '๐ŸŒก Thermometer'), ('๐ŸŒค', '๐ŸŒค Small Cloud'), ('๐ŸŒฅ', '๐ŸŒฅ White Sun Behind Cloud'), ('๐ŸŒฆ', '๐ŸŒฆ White Sun Behind Cloud With Rain'), ('๐ŸŒง', '๐ŸŒง Cloud With Rain'), ('๐ŸŒจ', '๐ŸŒจ Cloud With Snow'), ('๐ŸŒฉ', '๐ŸŒฉ Lightning'), ('๐ŸŒช', '๐ŸŒช Tornado'), ('๐ŸŒซ', '๐ŸŒซ Fog'), ('๐ŸŒฌ', '๐ŸŒฌ Blowing'), ('๐ŸŽ–', '๐ŸŽ– Medal'), ('๐ŸŽ—', '๐ŸŽ— Ribbon'), ('๐ŸŽž', '๐ŸŽž Film'), ('๐ŸŽŸ', '๐ŸŽŸ Admission Tickets'), ('๐Ÿท', '๐Ÿท Label'), ('๐ŸŒ', '๐ŸŒ Golfer'), ('๐Ÿ‹', '๐Ÿ‹ Lifting'), ('๐ŸŽ', '๐ŸŽ Racing Car'), ('๐Ÿ', '๐Ÿ Racing Motorcycle'), ('๐Ÿ…', '๐Ÿ… Medal'), ('๐Ÿ•น', '๐Ÿ•น Joystick'), ('โธ', 'โธ Double Vertical Bar'), ('โน', 'โน Black Square for Stop'), ('โบ', 'โบ Black Circle for Record'), ('๐ŸŽ™', '๐ŸŽ™ Microphone'), ('๐ŸŽš', '๐ŸŽš Level Slider'), ('๐ŸŽ›', '๐ŸŽ› Control Knobs'), ('๐Ÿ–ฅ', '๐Ÿ–ฅ Desktop'), ('๐Ÿ–จ', '๐Ÿ–จ Printer'), ('๐Ÿ–ฑ', '๐Ÿ–ฑ Three Button Mouse'), ('๐Ÿ–ฒ', '๐Ÿ–ฒ Trackball'), ('๐Ÿ“ฝ', '๐Ÿ“ฝ Film Projector'), ('๐Ÿ“ธ', '๐Ÿ“ธ Camera With Flash'), ('๐Ÿ•ฏ', '๐Ÿ•ฏ Candle'), ('๐Ÿ—ž', '๐Ÿ—ž Newspaper'), ('๐Ÿ—ณ', '๐Ÿ—ณ Ballot Box With Ballot'), ('๐Ÿ–‹', '๐Ÿ–‹ Fancy Pen'), ('๐Ÿ–Š', '๐Ÿ–Š Lower Left Ballpoint Pen'), ('๐Ÿ–Œ', '๐Ÿ–Œ Lower Left Paintbrush'), ('๐Ÿ–', '๐Ÿ– Lower Left Crayon'), ('๐Ÿ—‚', '๐Ÿ—‚ Card Index Dividers'), ('๐Ÿ—’', '๐Ÿ—’ Spiral Note Pad'), ('๐Ÿ—“', '๐Ÿ—“ Spiral Calendar Pad'), ('๐Ÿ–‡', '๐Ÿ–‡ Linked Paperclips'), ('๐Ÿ—ƒ', '๐Ÿ—ƒ Card File Box'), ('๐Ÿ—„', '๐Ÿ—„ File Cabinet'), ('๐Ÿ—‘', '๐Ÿ—‘ Wastebasket'), ('๐Ÿ—', '๐Ÿ— Old Key'), ('๐Ÿ› ', '๐Ÿ›  Tools'), ('๐Ÿ—œ', '๐Ÿ—œ Compression'), ('๐Ÿ—ก', '๐Ÿ—ก Dagger'), ('๐Ÿ›ก', '๐Ÿ›ก Shield'), ('๐Ÿณ', '๐Ÿณ White Flag'), ('๐Ÿด', '๐Ÿด Black Flag'), ('๐Ÿ•‰', '๐Ÿ•‰ Om Symbol'), ('๐Ÿ—จ', '๐Ÿ—จ Left Speech Bubble'), ('๐Ÿค—', '๐Ÿค— Hugging'), ('๐Ÿค”', '๐Ÿค” Thinking'), ('๐Ÿ™„', '๐Ÿ™„ Rolling Eyes'), ('๐Ÿค', '๐Ÿค Hushed'), ('๐Ÿค“', '๐Ÿค“ Nerd'), ('๐Ÿ™ƒ', '๐Ÿ™ƒ Upside Down'), ('๐Ÿค’', '๐Ÿค’ Sick'), ('๐Ÿค•', '๐Ÿค• Hurt Head'), ('๐Ÿค‘', '๐Ÿค‘ Money'), ('๐Ÿป', '๐Ÿป Emoji Modifier 1-2'), ('๐Ÿผ', '๐Ÿผ Emoji Modifier 3'), ('๐Ÿฝ', '๐Ÿฝ Emoji Modifier 4'), ('๐Ÿพ', '๐Ÿพ Emoji Modifier 5'), ('๐Ÿฟ', '๐Ÿฟ Emoji Modifier 6'), ('๐Ÿค˜', '๐Ÿค˜ Rock On'), ('๐Ÿ“ฟ', '๐Ÿ“ฟ Prayer Beads'), ('๐Ÿค–', '๐Ÿค– Robot'), ('๐Ÿฆ', '๐Ÿฆ Lion'), ('๐Ÿฆ„', '๐Ÿฆ„ Unicorn'), ('๐Ÿฆƒ', '๐Ÿฆƒ Turkey'), ('๐Ÿฆ€', '๐Ÿฆ€ Crab'), ('๐Ÿฆ‚', '๐Ÿฆ‚ Scorpion'), ('๐Ÿง€', '๐Ÿง€ Cheese'), ('๐ŸŒญ', '๐ŸŒญ Hot Dog'), ('๐ŸŒฎ', '๐ŸŒฎ Taco'), ('๐ŸŒฏ', '๐ŸŒฏ Burrito'), ('๐Ÿฟ', '๐Ÿฟ Popcorn'), ('๐Ÿพ', '๐Ÿพ Popping Cork'), ('๐Ÿบ', '๐Ÿบ Amphora'), ('๐Ÿ›', '๐Ÿ› Place of Worship'), ('๐Ÿ•‹', '๐Ÿ•‹ Kaaba'), ('๐Ÿ•Œ', '๐Ÿ•Œ Mosque'), ('๐Ÿ•', '๐Ÿ• Synagogue'), ('๐Ÿ•Ž', '๐Ÿ•Ž Menorah'), ('๐Ÿ', '๐Ÿ Bat and Ball'), ('๐Ÿ', '๐Ÿ Volleyball'), ('๐Ÿ‘', '๐Ÿ‘ Field Hockey'), ('๐Ÿ’', '๐Ÿ’ Ice Hockey'), ('๐Ÿ“', '๐Ÿ“ Table Tennis'), ('๐Ÿธ', '๐Ÿธ Badminton'), ('๐Ÿน', '๐Ÿน Archer'), ('๐Ÿคฃ', '๐Ÿคฃ ROFL Face'), ('๐Ÿคค', '๐Ÿคค Drooling'), ('๐Ÿคข', '๐Ÿคข Nauseated'), ('๐Ÿคง', '๐Ÿคง Sneezing'), ('๐Ÿค ', '๐Ÿค  Cowboy'), ('๐Ÿคก', '๐Ÿคก Clown'), ('๐Ÿคฅ', '๐Ÿคฅ Lying'), ('๐Ÿคด', '๐Ÿคด Prince'), ('๐Ÿคต', '๐Ÿคต Tuxedo Man'), ('๐Ÿคฐ', '๐Ÿคฐ Pregnant'), ('๐Ÿคถ', '๐Ÿคถ Mrs. Claus'), ('๐Ÿคฆ', '๐Ÿคฆ Facepalm'), ('๐Ÿคท', '๐Ÿคท Shrugging'), ('๐Ÿ•บ', '๐Ÿ•บ Man Dancing'), ('๐Ÿคบ', '๐Ÿคบ Fencing'), ('๐Ÿคธ', '๐Ÿคธ Cartwheel'), ('๐Ÿคผ', '๐Ÿคผ Wrestling'), ('๐Ÿคฝ', '๐Ÿคฝ Water Polo'), ('๐Ÿคพ', '๐Ÿคพ Handball'), ('๐Ÿคน', '๐Ÿคน Juggling'), ('๐Ÿคณ', '๐Ÿคณ Selfie'), ('๐Ÿคž', '๐Ÿคž Luck Hand'), ('๐Ÿค™', '๐Ÿค™ Call Me Hand'), ('๐Ÿค›', '๐Ÿค› Left-Facing Fist'), ('๐Ÿคœ', '๐Ÿคœ Right-Facing Fist'), ('๐Ÿคš', '๐Ÿคš Raised Back of Hand'), ('๐Ÿค', '๐Ÿค Business Hi'), ('๐Ÿ–ค', '๐Ÿ–ค Black Heart'), ('๐Ÿฆ', '๐Ÿฆ Gorilla'), ('๐ŸฆŠ', '๐ŸฆŠ Fox'), ('๐ŸฆŒ', '๐ŸฆŒ Deer'), ('๐Ÿฆ', '๐Ÿฆ Rhinoceros'), ('๐Ÿฆ‡', '๐Ÿฆ‡ Bat'), ('๐Ÿฆ…', '๐Ÿฆ… Eagle'), ('๐Ÿฆ†', '๐Ÿฆ† Duck'), ('๐Ÿฆ‰', '๐Ÿฆ‰ Owl'), ('๐ŸฆŽ', '๐ŸฆŽ Lizard'), ('๐Ÿฆˆ', '๐Ÿฆˆ Shark'), ('๐Ÿฆ', '๐Ÿฆ Shrimp'), ('๐Ÿฆ‘', '๐Ÿฆ‘ Squid'), ('๐Ÿฆ‹', '๐Ÿฆ‹ Butterfly'), ('๐Ÿฅ€', '๐Ÿฅ€ Wilted'), ('๐Ÿฅ', '๐Ÿฅ Kiwifruit'), ('๐Ÿฅ‘', '๐Ÿฅ‘ Pricey Fruit'), ('๐Ÿฅ”', '๐Ÿฅ” Potato'), ('๐Ÿฅ•', '๐Ÿฅ• Carrot'), ('๐Ÿฅ’', '๐Ÿฅ’ Cucumber'), ('๐Ÿฅœ', '๐Ÿฅœ Peanuts'), ('๐Ÿฅ', '๐Ÿฅ Croissant'), ('๐Ÿฅ–', '๐Ÿฅ– Bread Sword'), ('๐Ÿฅž', '๐Ÿฅž Pancakes'), ('๐Ÿฅ“', '๐Ÿฅ“ Bacon'), ('๐Ÿฅ™', '๐Ÿฅ™ Stuffed Flatbread'), ('๐Ÿฅš', '๐Ÿฅš Chicken Rock'), ('๐Ÿฅ˜', '๐Ÿฅ˜ Shallow Pan'), ('๐Ÿฅ—', '๐Ÿฅ— Salad'), ('๐Ÿฅ›', '๐Ÿฅ› Cow Juice'), ('๐Ÿฅ‚', '๐Ÿฅ‚ Clinking Glasses'), ('๐Ÿฅƒ', '๐Ÿฅƒ Tumbler'), ('๐Ÿฅ„', '๐Ÿฅ„ Spoon'), ('๐Ÿ›ด', '๐Ÿ›ด Scoot Scoot'), ('๐Ÿ›ต', '๐Ÿ›ต Motor Scooter'), ('๐Ÿ›‘', '๐Ÿ›‘ Stop Sign'), ('๐Ÿ›ถ', '๐Ÿ›ถ Canoe'), ('๐Ÿฅ‡', '๐Ÿฅ‡ Gold Medal'), ('๐Ÿฅˆ', '๐Ÿฅˆ Silver Medal'), ('๐Ÿฅ‰', '๐Ÿฅ‰ Participation'), ('๐ŸฅŠ', '๐ŸฅŠ Boxing'), ('๐Ÿฅ‹', '๐Ÿฅ‹ Martial Arts'), ('๐Ÿฅ…', '๐Ÿฅ… Hashtag Goals'), ('๐Ÿฅ', '๐Ÿฅ Drum Roll'), ('๐Ÿ›’', '๐Ÿ›’ Food Ute'), ('๐Ÿคฉ', '๐Ÿคฉ Star Struck'), ('๐Ÿคจ', '๐Ÿคจ Unexpected Face'), ('๐Ÿคฏ', '๐Ÿคฏ Mind Blown'), ('๐Ÿคช', '๐Ÿคช Zany Face'), ('๐Ÿคฌ', '๐Ÿคฌ Swear Face'), ('๐Ÿคฎ', '๐Ÿคฎ Vomiting'), ('๐Ÿคซ', '๐Ÿคซ Shushing'), ('๐Ÿคญ', '๐Ÿคญ Hand Over Mouth'), ('๐Ÿง', '๐Ÿง Monocle'), ('๐Ÿง’', '๐Ÿง’ Child Face'), ('๐Ÿง‘', '๐Ÿง‘ Adult'), ('๐Ÿง“', '๐Ÿง“ Older Adult'), ('๐Ÿง•', '๐Ÿง• Headscarf'), ('๐Ÿง”', '๐Ÿง” Bearded Person'), ('๐Ÿคฑ', '๐Ÿคฑ Breast Feeding'), ('๐Ÿง™', '๐Ÿง™ Mage'), ('๐Ÿงš', '๐Ÿงš Fairy'), ('๐Ÿง›', '๐Ÿง› Vampire'), ('๐Ÿงœ', '๐Ÿงœ Merperson'), ('๐Ÿง', '๐Ÿง Cosplay'), ('๐Ÿงž', '๐Ÿงž Genie'), ('๐ŸงŸ', '๐ŸงŸ Unalive'), ('๐Ÿง–', '๐Ÿง– Steamy Room'), ('๐Ÿง—', '๐Ÿง— Person Climbing'), ('๐Ÿง˜', '๐Ÿง˜ Lotus Position'), ('๐ŸคŸ', '๐ŸคŸ Love-You Gesture'), ('๐Ÿคฒ', '๐Ÿคฒ Palms Up Together'), ('๐Ÿง ', '๐Ÿง  Big Brain'), ('๐Ÿงก', '๐Ÿงก Orange Heart'), ('๐Ÿงฃ', '๐Ÿงฃ Neck Hider'), ('๐Ÿงค', '๐Ÿงค Hand Socks'), ('๐Ÿงฅ', '๐Ÿงฅ Coat'), ('๐Ÿงฆ', '๐Ÿงฆ Feet Gloves'), ('๐Ÿงข', '๐Ÿงข Billed Cap'), ('๐Ÿฆ“', '๐Ÿฆ“ Zebra'), ('๐Ÿฆ’', '๐Ÿฆ’ Giraffe'), ('๐Ÿฆ”', '๐Ÿฆ” Spikehog'), ('๐Ÿฆ•', '๐Ÿฆ• Long Neck'), ('๐Ÿฆ–', '๐Ÿฆ– Big Roar'), ('๐Ÿฆ—', '๐Ÿฆ— Cricket'), ('๐Ÿฅฅ', '๐Ÿฅฅ Coconut'), ('๐Ÿฅฆ', '๐Ÿฅฆ Tiny Tree'), ('๐Ÿฅจ', '๐Ÿฅจ Twisty Bread'), ('๐Ÿฅฉ', '๐Ÿฅฉ Cut of Meat'), ('๐Ÿฅช', '๐Ÿฅช Sandwich'), ('๐Ÿฅฃ', '๐Ÿฅฃ Bowl With Spoon'), ('๐Ÿฅซ', '๐Ÿฅซ Canned Good'), ('๐ŸฅŸ', '๐ŸฅŸ Dumpling'), ('๐Ÿฅ ', '๐Ÿฅ  Tasty Future'), ('๐Ÿฅก', '๐Ÿฅก Takeout Box'), ('๐Ÿฅง', '๐Ÿฅง Pie'), ('๐Ÿฅค', '๐Ÿฅค Cup With Straw'), ('๐Ÿฅข', '๐Ÿฅข Chopsticks'), ('๐Ÿ›ธ', '๐Ÿ›ธ Alien Plane'), ('๐Ÿ›ท', '๐Ÿ›ท Sled'), ('๐ŸฅŒ', '๐ŸฅŒ Curling'), ('๐Ÿฅฐ', '๐Ÿฅฐ Smiling Face With 3 Hearts'), ('๐Ÿฅต', '๐Ÿฅต Overheated'), ('๐Ÿฅถ', '๐Ÿฅถ Freezing Face'), ('๐Ÿฅด', '๐Ÿฅด Woozy Face'), ('๐Ÿฅณ', '๐Ÿฅณ Party Face'), ('๐Ÿฅบ', '๐Ÿฅบ Pleading Face'), ('๐Ÿฆต', '๐Ÿฆต Leg'), ('๐Ÿฆถ', '๐Ÿฆถ Foot'), ('๐Ÿฆท', '๐Ÿฆท Tooth'), ('๐Ÿฆด', '๐Ÿฆด Bone'), ('๐Ÿฆธ', '๐Ÿฆธ Superhero'), ('๐Ÿฆน', '๐Ÿฆน Supervillain'), ('๐Ÿฆ', '๐Ÿฆ Trash Bandit'), ('๐Ÿฆ™', '๐Ÿฆ™ Llama'), ('๐Ÿฆ›', '๐Ÿฆ› Hippopotamus'), ('๐Ÿฆ˜', '๐Ÿฆ˜ Kangaroo'), ('๐Ÿฆก', '๐Ÿฆก Badger'), ('๐Ÿฆข', '๐Ÿฆข Swan'), ('๐Ÿฆš', '๐Ÿฆš Peacock'), ('๐Ÿฆœ', '๐Ÿฆœ Parrot'), ('๐ŸฆŸ', '๐ŸฆŸ Mosquito'), ('๐Ÿฆ ', '๐Ÿฆ  Microbe'), ('๐Ÿฅญ', '๐Ÿฅญ Mango'), ('๐Ÿฅฌ', '๐Ÿฅฌ Leafy Green'), ('๐Ÿฅฏ', '๐Ÿฅฏ Bagel'), ('๐Ÿง‚', '๐Ÿง‚ Salty'), ('๐Ÿฅฎ', '๐Ÿฅฎ Moon Cake'), ('๐Ÿฆž', '๐Ÿฆž Lobster'), ('๐Ÿง', '๐Ÿง Cupcake'), ('๐Ÿงญ', '๐Ÿงญ Compass'), ('๐Ÿงฑ', '๐Ÿงฑ Brick'), ('๐Ÿ›น', '๐Ÿ›น Skateboard'), ('๐Ÿงณ', '๐Ÿงณ Baggage'), ('๐Ÿงจ', '๐Ÿงจ Firework'), ('๐Ÿงง', '๐Ÿงง Red Envelope'), ('๐ŸฅŽ', '๐ŸฅŽ Softball'), ('๐Ÿฅ', '๐Ÿฅ Throwing Disc'), ('๐Ÿฅ', '๐Ÿฅ Lacrosse'), ('๐Ÿงฟ', '๐Ÿงฟ Nazar Amulet'), ('๐Ÿงฉ', '๐Ÿงฉ Puzzle Piece'), ('๐Ÿงธ', '๐Ÿงธ Teddy Bear'), ('๐Ÿงต', '๐Ÿงต Thread'), ('๐Ÿงถ', '๐Ÿงถ Yarn Ball'), ('๐Ÿฅฝ', '๐Ÿฅฝ The Goggles'), ('๐Ÿฅผ', '๐Ÿฅผ Lab Coat'), ('๐Ÿฅพ', '๐Ÿฅพ Hiking Boot'), ('๐Ÿฅฟ', '๐Ÿฅฟ Flat Shoe'), ('๐Ÿงฎ', '๐Ÿงฎ Abacus'), ('๐Ÿงพ', '๐Ÿงพ Receipt'), ('๐Ÿงฐ', '๐Ÿงฐ Toolbox'), ('๐Ÿงฒ', '๐Ÿงฒ Magnet'), ('๐Ÿงช', '๐Ÿงช Test Tube'), ('๐Ÿงซ', '๐Ÿงซ Petri Dish'), ('๐Ÿงฌ', '๐Ÿงฌ DNA'), ('๐Ÿงด', '๐Ÿงด Lotion'), ('๐Ÿงท', '๐Ÿงท Safety Pin'), ('๐Ÿงน', '๐Ÿงน Broom'), ('๐Ÿงบ', '๐Ÿงบ Basket'), ('๐Ÿงป', '๐Ÿงป Roll of Paper'), ('๐Ÿงผ', '๐Ÿงผ Soap'), ('๐Ÿงฝ', '๐Ÿงฝ Fun sponge'), ('๐Ÿงฏ', '๐Ÿงฏ Anti-fire Can'), ('๐Ÿฅฑ', '๐Ÿฅฑ Yawning Face'), ('๐ŸคŽ', '๐ŸคŽ Brown Heart'), ('๐Ÿค', '๐Ÿค White Heart'), ('๐Ÿค', '๐Ÿค Pinching Hand'), ('๐Ÿฆพ', '๐Ÿฆพ Mechanical Arm'), ('๐Ÿฆฟ', '๐Ÿฆฟ Mechanical Leg'), ('๐Ÿฆป', '๐Ÿฆป Ear with Hearing Aid'), ('๐Ÿง', '๐Ÿง Deaf Person'), ('๐Ÿง', '๐Ÿง Person Standing'), ('๐ŸงŽ', '๐ŸงŽ Person Kneeling'), ('๐Ÿฆง', '๐Ÿฆง Orangutan'), ('๐Ÿฆฎ', '๐Ÿฆฎ Guide Dog'), ('๐Ÿฆฅ', '๐Ÿฆฅ Lazy Tree Dog'), ('๐Ÿฆฆ', '๐Ÿฆฆ Water Dog'), ('๐Ÿฆจ', '๐Ÿฆจ Stinky dog'), ('๐Ÿฆฉ', '๐Ÿฆฉ Pink Dog'), ('๐Ÿง„', '๐Ÿง„ Garlic'), ('๐Ÿง…', '๐Ÿง… Onion'), ('๐Ÿง‡', '๐Ÿง‡ Waffle'), ('๐Ÿง†', '๐Ÿง† Falafel'), ('๐Ÿงˆ', '๐Ÿงˆ Butter'), ('๐Ÿฆช', '๐Ÿฆช Oyster'), ('๐Ÿงƒ', '๐Ÿงƒ Beverage Box'), ('๐Ÿง‰', '๐Ÿง‰ Mate'), ('๐ŸงŠ', '๐ŸงŠ Cold Cuboid'), ('๐Ÿ›•', '๐Ÿ›• Hindu Temple'), ('๐Ÿฆฝ', '๐Ÿฆฝ Manual Wheelchair'), ('๐Ÿฆผ', '๐Ÿฆผ Motorized Wheelchair'), ('๐Ÿ›บ', '๐Ÿ›บ Auto Rickshaw'), ('๐Ÿช‚', '๐Ÿช‚ Parachute'), ('๐Ÿช', '๐Ÿช Ringed Planet'), ('๐Ÿคฟ', '๐Ÿคฟ Diving Mask'), ('๐Ÿช€', '๐Ÿช€ Yo-Yo'), ('๐Ÿช', '๐Ÿช Kite'), ('๐Ÿฆบ', '๐Ÿฆบ Safety Vest'), ('๐Ÿฅป', '๐Ÿฅป Sari'), ('๐Ÿฉฑ', '๐Ÿฉฑ One-Piece Swimsuit'), ('๐Ÿฉฒ', '๐Ÿฉฒ Briefs'), ('๐Ÿฉณ', '๐Ÿฉณ Shorts'), ('๐Ÿฉฐ', '๐Ÿฉฐ Ballet Shoes'), ('๐Ÿช•', '๐Ÿช• Banjo'), ('๐Ÿช”', '๐Ÿช” Diya Lamp'), ('๐Ÿช“', '๐Ÿช“ Axe'), ('๐Ÿฆฏ', '๐Ÿฆฏ White Cane'), ('๐Ÿฉธ', '๐Ÿฉธ Drop of Blood'), ('๐Ÿฉน', '๐Ÿฉน Adhesive Bandage'), ('๐Ÿฉบ', '๐Ÿฉบ Stethoscope'), ('๐Ÿช‘', '๐Ÿช‘ Chair'), ('๐Ÿช’', '๐Ÿช’ Razor'), ('๐ŸŸ ', '๐ŸŸ  Orange Circle'), ('๐ŸŸก', '๐ŸŸก Yellow Circle'), ('๐ŸŸข', '๐ŸŸข Green Circle'), ('๐ŸŸฃ', '๐ŸŸฃ Purple Circle'), ('๐ŸŸค', '๐ŸŸค Brown Circle'), ('๐ŸŸฅ', '๐ŸŸฅ Red Square'), ('๐ŸŸง', '๐ŸŸง Orange Square'), ('๐ŸŸจ', '๐ŸŸจ Yellow Square'), ('๐ŸŸฉ', '๐ŸŸฉ Green Square'), ('๐ŸŸฆ', '๐ŸŸฆ Blue Square'), ('๐ŸŸช', '๐ŸŸช Purple Square'), ('๐ŸŸซ', '๐ŸŸซ Brown Square'), ('๐Ÿฅฒ', '๐Ÿฅฒ Smiling Face with Tear'), ('๐Ÿฅธ', '๐Ÿฅธ Disguised Face'), ('๐ŸคŒ', '๐ŸคŒ Pinched Fingers'), ('๐Ÿซ€', '๐Ÿซ€ Anatomical Heart'), ('๐Ÿซ', '๐Ÿซ Lungs'), ('๐Ÿฅท', '๐Ÿฅท Ninja'), ('๐Ÿซ‚', '๐Ÿซ‚ People Hugging'), ('๐Ÿฆฌ', '๐Ÿฆฌ Bison'), ('๐Ÿฆฃ', '๐Ÿฆฃ Mammoth'), ('๐Ÿฆซ', '๐Ÿฆซ Beaver'), ('๐Ÿฆค', '๐Ÿฆค Dodo'), ('๐Ÿชถ', '๐Ÿชถ Feather'), ('๐Ÿฆญ', '๐Ÿฆญ Seal'), ('๐Ÿชฒ', '๐Ÿชฒ Beetle'), ('๐Ÿชณ', '๐Ÿชณ Cockroach'), ('๐Ÿชฐ', '๐Ÿชฐ Fly'), ('๐Ÿชฑ', '๐Ÿชฑ Worm'), ('๐Ÿชด', '๐Ÿชด Potted Plant'), ('๐Ÿซ', '๐Ÿซ Blueberries'), ('๐Ÿซ’', '๐Ÿซ’ Olive'), ('๐Ÿซ‘', '๐Ÿซ‘ Bell Pepper'), ('๐Ÿซ“', '๐Ÿซ“ Flatbread'), ('๐Ÿซ”', '๐Ÿซ” Tamale'), ('๐Ÿซ•', '๐Ÿซ• Fondue'), ('๐Ÿซ–', '๐Ÿซ– Teapot'), ('๐Ÿง‹', '๐Ÿง‹ Bubble Tea'), ('๐Ÿชจ', '๐Ÿชจ Rock'), ('๐Ÿชต', '๐Ÿชต Wood'), ('๐Ÿ›–', '๐Ÿ›– Hut'), ('๐Ÿ›ป', '๐Ÿ›ป Pickup Truck'), ('๐Ÿ›ผ', '๐Ÿ›ผ Roller Skate'), ('๐Ÿช„', '๐Ÿช„ Magic Wand'), ('๐Ÿช…', '๐Ÿช… Piรฑata'), ('๐Ÿช†', '๐Ÿช† Nesting Dolls'), ('๐Ÿชก', '๐Ÿชก Sewing Needle'), ('๐Ÿชข', '๐Ÿชข Knot'), ('๐Ÿฉด', '๐Ÿฉด Thong Sandal'), ('๐Ÿช–', '๐Ÿช– Military Helmet'), ('๐Ÿช—', '๐Ÿช— Accordion'), ('๐Ÿช˜', '๐Ÿช˜ Long Drum'), ('๐Ÿช™', '๐Ÿช™ Coin'), ('๐Ÿชƒ', '๐Ÿชƒ Boomerang'), ('๐Ÿชš', '๐Ÿชš Carpentry Saw'), ('๐Ÿช›', '๐Ÿช› Screwdriver'), ('๐Ÿช', '๐Ÿช Hook'), ('๐Ÿชœ', '๐Ÿชœ Ladder'), ('๐Ÿ›—', '๐Ÿ›— Elevator'), ('๐Ÿชž', '๐Ÿชž Mirror'), ('๐ŸชŸ', '๐ŸชŸ Window'), ('๐Ÿช ', '๐Ÿช  Plunger'), ('๐Ÿชค', '๐Ÿชค Mouse Trap'), ('๐Ÿชฃ', '๐Ÿชฃ Bucket'), ('๐Ÿชฅ', '๐Ÿชฅ Toothbrush'), ('๐Ÿชฆ', '๐Ÿชฆ Headstone'), ('๐Ÿชง', '๐Ÿชง Placard')], default=None, max_length=3, null=True, verbose_name='emoji'), + ), + migrations.AddConstraint( + model_name='speakercategory', + constraint=utils.models.UniqueConstraint(fields=('tournament', 'seq'), name='partici_speakercategory_tournament__seq_uniq'), + ), + migrations.AddConstraint( + model_name='speakercategory', + constraint=utils.models.UniqueConstraint(fields=('tournament', 'slug'), name='partici_speakercategory_tournament__slug_uniq'), + ), + migrations.AddConstraint( + model_name='team', + constraint=utils.models.UniqueConstraint(fields=('reference', 'institution', 'tournament'), name='partici_team_reference__institution__tournament_uniq'), + ), + migrations.AddConstraint( + model_name='team', + constraint=utils.models.UniqueConstraint(fields=('emoji', 'tournament'), name='partici_team_emoji__tournament_uniq'), + ), + ] diff --git a/tabbycat/participants/migrations/0023_alter_institution_unique_together_and_more.py b/tabbycat/participants/migrations/0023_alter_institution_unique_together_and_more.py new file mode 100644 index 00000000000..52df11008d7 --- /dev/null +++ b/tabbycat/participants/migrations/0023_alter_institution_unique_together_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.4 on 2024-05-04 21:38 + +import utils.models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('participants', '0022_rename_team_tournament_institution_short_reference_participant_tournam_160efa_idx_and_more'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='institution', + unique_together=set(), + ), + migrations.AddConstraint( + model_name='institution', + constraint=utils.models.UniqueConstraint(fields=('name', 'code'), name='partici_institution_name__code_uniq'), + ), + ] diff --git a/tabbycat/participants/models.py b/tabbycat/participants/models.py index 2cf1f245a61..5d70e909dc8 100644 --- a/tabbycat/participants/models.py +++ b/tabbycat/participants/models.py @@ -10,6 +10,7 @@ from django.utils.translation import gettext_lazy as _ from utils.managers import LookupByNameFieldsMixin +from utils.models import UniqueConstraint from .emoji import EMOJI_FIELD_CHOICES @@ -50,7 +51,9 @@ class Institution(models.Model): objects = InstitutionManager() class Meta: - unique_together = [('name', 'code')] + constraints = [ + UniqueConstraint(fields=['name', 'code']), + ] ordering = ['name'] verbose_name = _("institution") verbose_name_plural = _("institutions") @@ -81,9 +84,11 @@ class SpeakerCategory(models.Model): help_text=_("If checked, this category will be included in the speaker category tabs shown to the public")) class Meta: - unique_together = [('tournament', 'seq'), ('tournament', 'slug')] + constraints = [ + UniqueConstraint(fields=['tournament', 'seq']), + UniqueConstraint(fields=['tournament', 'slug']), + ] ordering = ['tournament', 'seq'] - index_together = ['tournament', 'seq'] verbose_name = _("speaker category") verbose_name_plural = _("speaker categories") @@ -199,17 +204,17 @@ class Team(models.Model): verbose_name=_("emoji")) class Meta: - unique_together = [ + constraints = [ # Enforce for blank references also - two teams from the same # institution can't both be unlabelled. However, Django won't # enforce this for null institutions. - ('reference', 'institution', 'tournament'), + UniqueConstraint(fields=['reference', 'institution', 'tournament']), # Not enforced for blank emoji (null=True is set on emoji) - ('emoji', 'tournament'), + UniqueConstraint(fields=['emoji', 'tournament']), ] + indexes = [models.Index(fields=['tournament', 'institution', 'short_reference'])] ordering = ['tournament', 'institution', 'short_reference'] - index_together = ['tournament', 'institution', 'short_reference'] verbose_name = _("team") verbose_name_plural = _("teams") diff --git a/tabbycat/results/migrations/0016_rename_speakerscorebyadj_ballot_submission_debate_adjudicator_results_spe_ballot__667598_idx_and_mor.py b/tabbycat/results/migrations/0016_rename_speakerscorebyadj_ballot_submission_debate_adjudicator_results_spe_ballot__667598_idx_and_mor.py new file mode 100644 index 00000000000..08a2a41b815 --- /dev/null +++ b/tabbycat/results/migrations/0016_rename_speakerscorebyadj_ballot_submission_debate_adjudicator_results_spe_ballot__667598_idx_and_mor.py @@ -0,0 +1,70 @@ +# Generated by Django 5.0.4 on 2024-05-04 13:21 + +import utils.models +from django.conf import settings +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('adjallocation', '0010_alter_adjudicatoradjudicatorconflict_unique_together_and_more'), + ('draw', '0009_alter_teamsideallocation_unique_together_and_more'), + ('motions', '0006_alter_debateteammotionpreference_unique_together_and_more'), + ('participants', '0022_rename_team_tournament_institution_short_reference_participant_tournam_160efa_idx_and_more'), + ('results', '0015_alter_ballotsubmission_submitter_type'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.RenameIndex( + model_name='speakerscorebyadj', + new_name='results_spe_ballot__667598_idx', + old_fields=('ballot_submission', 'debate_adjudicator'), + ), + migrations.RenameIndex( + model_name='teamscorebyadj', + new_name='results_tea_ballot__a296a6_idx', + old_fields=('ballot_submission', 'debate_adjudicator'), + ), + migrations.AlterUniqueTogether( + name='ballotsubmission', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='speakerscore', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='speakerscorebyadj', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='teamscore', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='teamscorebyadj', + unique_together=set(), + ), + migrations.AddConstraint( + model_name='ballotsubmission', + constraint=utils.models.UniqueConstraint(fields=('debate', 'version'), name='results_ballotsubmission_debate__version_uniq'), + ), + migrations.AddConstraint( + model_name='speakerscore', + constraint=utils.models.UniqueConstraint(fields=('debate_team', 'position', 'ballot_submission'), name='results_speakerscore_debate_team__position__ballot_submission_uniq'), + ), + migrations.AddConstraint( + model_name='speakerscorebyadj', + constraint=utils.models.UniqueConstraint(fields=('debate_adjudicator', 'debate_team', 'position', 'ballot_submission'), name='results_speakerscorebyadj_debate_adjudicator__debate_team__position__ballot_submission_uniq'), + ), + migrations.AddConstraint( + model_name='teamscore', + constraint=utils.models.UniqueConstraint(fields=('debate_team', 'ballot_submission'), name='results_teamscore_debate_team__ballot_submission_uniq'), + ), + migrations.AddConstraint( + model_name='teamscorebyadj', + constraint=utils.models.UniqueConstraint(fields=('debate_adjudicator', 'debate_team', 'ballot_submission'), name='results_teamscorebyadj_debate_adjudicator__debate_team__ballot_submission_uniq'), + ), + ] diff --git a/tabbycat/results/models.py b/tabbycat/results/models.py index 4dcac686239..0207abe13b6 100644 --- a/tabbycat/results/models.py +++ b/tabbycat/results/models.py @@ -9,6 +9,7 @@ from motions.models import RoundMotion from utils.misc import badge_datetime_format, reverse_tournament +from utils.models import UniqueConstraint from .result import DebateResult from .utils import readable_ballotsub_result @@ -67,7 +68,7 @@ class Meta: @property def _unique_filter_args(self): - return dict((arg, getattr(self, arg)) for arg in self._meta.unique_together[0] + return dict((arg, getattr(self, arg)) for arg in self._meta.constraints[0].fields if arg != 'version') def _unique_unconfirm_args(self): @@ -117,7 +118,7 @@ class BallotSubmission(Submission): "when individual adjudicator ballots are enabled.")) class Meta: - unique_together = [('debate', 'version')] + constraints = [UniqueConstraint(fields=['debate', 'version'])] verbose_name = _("ballot submission") verbose_name_plural = _("ballot submissions") @@ -235,8 +236,10 @@ class TeamScoreByAdj(models.Model): verbose_name=_("score")) class Meta: - unique_together = [('debate_adjudicator', 'debate_team', 'ballot_submission')] - index_together = ['ballot_submission', 'debate_adjudicator'] + constraints = [ + UniqueConstraint(fields=['debate_adjudicator', 'debate_team', 'ballot_submission']), + ] + indexes = [models.Index(fields=['ballot_submission', 'debate_adjudicator'])] verbose_name = _("team score by adjudicator") verbose_name_plural = _("team scores by adjudicator") @@ -269,9 +272,10 @@ class SpeakerScoreByAdj(models.Model): position = models.IntegerField(verbose_name=_("position")) class Meta: - unique_together = [('debate_adjudicator', 'debate_team', 'position', - 'ballot_submission')] - index_together = ['ballot_submission', 'debate_adjudicator'] + constraints = [ + UniqueConstraint(fields=['debate_adjudicator', 'debate_team', 'position', 'ballot_submission']), + ] + indexes = [models.Index(fields=['ballot_submission', 'debate_adjudicator'])] verbose_name = _("speaker score by adjudicator") verbose_name_plural = _("speaker scores by adjudicator") @@ -317,7 +321,7 @@ class TeamScore(models.Model): has_ghost = models.BooleanField(null=True, blank=True, verbose_name=_("has ghost score")) class Meta: - unique_together = [('debate_team', 'ballot_submission')] + constraints = [UniqueConstraint(fields=['debate_team', 'ballot_submission'])] verbose_name = _("team score") verbose_name_plural = _("team scores") @@ -360,7 +364,7 @@ class SpeakerScore(models.Model): objects = SpeakerScoreManager() class Meta: - unique_together = [('debate_team', 'position', 'ballot_submission')] + constraints = [UniqueConstraint(fields=['debate_team', 'position', 'ballot_submission'])] verbose_name = _("speaker score") verbose_name_plural = _("speaker scores") diff --git a/tabbycat/settings/core.py b/tabbycat/settings/core.py index a7ccbec4926..2f691567e48 100644 --- a/tabbycat/settings/core.py +++ b/tabbycat/settings/core.py @@ -32,7 +32,6 @@ USE_I18N = True USE_TZ = True -USE_L10N = True LANGUAGE_CODE = 'en' TIME_ZONE = os.environ.get('TIME_ZONE', 'Australia/Melbourne') @@ -231,7 +230,14 @@ 'django.contrib.staticfiles.finders.AppDirectoriesFinder', ) -STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' +STORAGES = { + "default": { + "BACKEND": "django.core.files.storage.FileSystemStorage", + }, + "staticfiles": { + "BACKEND": 'whitenoise.storage.CompressedManifestStaticFilesStorage', + }, +} # ============================================================================== # Logging diff --git a/tabbycat/standings/teams.py b/tabbycat/standings/teams.py index 019b65287c5..5958e5eea46 100644 --- a/tabbycat/standings/teams.py +++ b/tabbycat/standings/teams.py @@ -190,7 +190,7 @@ def annotate(self, queryset, standings, round=None): filter=opponents_filter) logger.info("Opponents annotation: %s", str(opponents_annotation)) teams_with_opponents = queryset.model.objects.annotate(opponent_ids=opponents_annotation) - opponents_by_team = {team.id: team.opponent_ids for team in teams_with_opponents} + opponents_by_team = {team.id: team.opponent_ids or [] for team in teams_with_opponents} opp_metric_queryset = self.opponent_annotator().get_annotated_queryset( queryset[0].tournament.team_set.all(), round) diff --git a/tabbycat/tournaments/migrations/0011_alter_round_unique_together_and_more.py b/tabbycat/tournaments/migrations/0011_alter_round_unique_together_and_more.py new file mode 100644 index 00000000000..7f45645e93f --- /dev/null +++ b/tabbycat/tournaments/migrations/0011_alter_round_unique_together_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 5.0.4 on 2024-05-04 13:21 + +import utils.models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('breakqual', '0006_alter_breakcategory_unique_together_and_more'), + ('tournaments', '0010_alter_round_draw_type'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='round', + unique_together=set(), + ), + migrations.AlterIndexTogether( + name='round', + index_together=set(), + ), + migrations.AddConstraint( + model_name='round', + constraint=utils.models.UniqueConstraint(fields=('tournament', 'seq'), name='tournam_round_tournament__seq_uniq'), + ), + ] diff --git a/tabbycat/tournaments/models.py b/tabbycat/tournaments/models.py index 272b25736c6..d54f6955285 100644 --- a/tabbycat/tournaments/models.py +++ b/tabbycat/tournaments/models.py @@ -9,6 +9,7 @@ from participants.models import Person from utils.managers import LookupByNameFieldsMixin +from utils.models import UniqueConstraint logger = logging.getLogger(__name__) @@ -338,11 +339,10 @@ class Status(models.TextChoices): help_text=_("A factor for the points received in the round. For example, if 2, all points are doubled.")) class Meta: + constraints = [UniqueConstraint(fields=['tournament', 'seq'])] verbose_name = _('round') verbose_name_plural = _('rounds') - unique_together = [('tournament', 'seq')] ordering = ['tournament', 'seq'] - index_together = ['tournament', 'seq'] def __str__(self): return "[%s] %s" % (self.tournament, self.name) diff --git a/tabbycat/utils/models.py b/tabbycat/utils/models.py new file mode 100644 index 00000000000..1e121ee5f83 --- /dev/null +++ b/tabbycat/utils/models.py @@ -0,0 +1,8 @@ +from django.db import models + + +class UniqueConstraint(models.UniqueConstraint): + def __init__(self, *expressions, fields=(), name=None, **kwargs): + if name is None: + name = '%(app_label).7s_%(class)s_' + "__".join(fields) + '_uniq' + return super().__init__(*expressions, fields=fields, name=name, **kwargs) diff --git a/tabbycat/venues/migrations/0011_alter_venue_options_alter_venue_index_together_and_more.py b/tabbycat/venues/migrations/0011_alter_venue_options_alter_venue_index_together_and_more.py new file mode 100644 index 00000000000..3a933d074fe --- /dev/null +++ b/tabbycat/venues/migrations/0011_alter_venue_options_alter_venue_index_together_and_more.py @@ -0,0 +1,26 @@ +# Generated by Django 5.0.4 on 2024-05-04 13:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tournaments', '0011_alter_round_unique_together_and_more'), + ('venues', '0010_populate_tournaments'), + ] + + operations = [ + migrations.AlterModelOptions( + name='venue', + options={'ordering': ['tournament', 'name'], 'verbose_name': 'room', 'verbose_name_plural': 'rooms'}, + ), + migrations.AlterIndexTogether( + name='venue', + index_together=set(), + ), + migrations.AddIndex( + model_name='venue', + index=models.Index(fields=['tournament', 'name'], name='venues_venu_tournam_401b28_idx'), + ), + ] diff --git a/tabbycat/venues/models.py b/tabbycat/venues/models.py index a1c02761fb8..3b131a8a3cb 100644 --- a/tabbycat/venues/models.py +++ b/tabbycat/venues/models.py @@ -21,8 +21,8 @@ class Venue(models.Model): round_availabilities = GenericRelation('availability.RoundAvailability') class Meta: - ordering = ['name'] - index_together = ['name'] + indexes = [models.Index(fields=['tournament', 'name'])] + ordering = ['tournament', 'name'] verbose_name = _("room") verbose_name_plural = _("rooms") From a6e98d5e988a582850d2bbed9cf13473e4645d67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Sun, 12 May 2024 14:15:18 -0300 Subject: [PATCH 22/27] Update caching on membership changes --- tabbycat/users/migrations/0001_initial.py | 563 ++++++++++++++++++++-- tabbycat/users/models.py | 19 +- tabbycat/users/permissions.py | 13 +- 3 files changed, 561 insertions(+), 34 deletions(-) diff --git a/tabbycat/users/migrations/0001_initial.py b/tabbycat/users/migrations/0001_initial.py index e9c406c5073..6314c0765b8 100644 --- a/tabbycat/users/migrations/0001_initial.py +++ b/tabbycat/users/migrations/0001_initial.py @@ -1,9 +1,9 @@ -# Generated by Django 4.1.7 on 2023-07-24 16:24 +# Generated by Django 5.0.4 on 2024-05-12 17:19 -from django.conf import settings -from django.db import migrations, models import django.db.models.deletion import utils.fields +from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): @@ -11,50 +11,559 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ("tournaments", "0010_alter_round_draw_type"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('tournaments', '0010_alter_round_draw_type'), ] operations = [ migrations.CreateModel( - name='Group', + name="Group", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100, verbose_name='name')), - ('permissions', utils.fields.ChoiceArrayField(base_field=models.CharField(choices=[('view.adjudicatorteamconflict', 'view adjudicator-team conflicts'), ('edit.adjudicatorteamconflict', 'edit adjudicator-team conflicts'), ('view.adjudicatoradjudicatorconflict', 'view adjudicator-adjudicator conflicts'), ('edit.adjudicatoradjudicatorconflict', 'edit adjudicator-adjudicator conflicts'), ('view.adjudicatorinstitutionconflict', 'view adjudicator-institution conflicts'), ('edit.adjudicatorinstitutionconflict', 'edit adjudicator-institution conflicts'), ('view.teaminstitutionconflict', 'view team-institution conflicts'), ('edit.teaminstitutionconflict', 'edit team-institution conflicts'), ('view.actionlogentry', 'view action log entries'), ('view.team', 'view teams'), ('add.team', 'add teams'), ('view.roundavailability.team', 'view round availabilities for teams'), ('view.roundavailability.adjudicator', 'view round availabilities for adjudicators'), ('view.roundavailability.venue', 'view round availabilities for rooms'), ('edit.roundavailability.team', 'edit round availabilities for teams'), ('edit.roundavailability.adjudicator', 'edit round availabilities for adjudicators'), ('edit.roundavailability.venue', 'edit round availabilities for rooms'), ('view.debate', 'view debates (draw)'), ('view.debate.admin', 'view debates (detailed draw)'), ('generate.debate', 'generate debates (draw)'), ('edit.debateteam', 'edit debate teams (pairings)'), ('view.debateadjudicator', 'view debate adjudicators (allocations)'), ('edit.debateadjudicator', 'edit debate adjudicators (allocations)'), ('view.ballotsubmission.new', 'view confirmed ballots'), ('edit.ballotsubmission.old', 'edit non-confirmed ballots'), ('view.ballotsubmission', 'view any ballot'), ('edit.ballotsubmission', 'edit any ballot'), ('add.ballotsubmission', 'create ballots'), ('mark.ballotsubmission', 'confirm/discard any ballot'), ('mark.ballotsubmission.others', "confirm/discard others' ballots"), ('view.ballotsubmission.graph', 'view ballot graph'), ('view.tournamentpreferencemodel', 'view tournament configuration'), ('edit.tournamentpreferencemodel', 'edit tournament configuration')], max_length=50), blank=True, default=list, size=None, verbose_name='permissions')), - ('tournament', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tournaments.tournament', verbose_name='tournament')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100, verbose_name="name")), + ( + "permissions", + utils.fields.ChoiceArrayField( + base_field=models.CharField( + choices=[ + ( + "view.adjudicatorteamconflict", + "view adjudicator-team conflicts", + ), + ( + "edit.adjudicatorteamconflict", + "edit adjudicator-team conflicts", + ), + ( + "view.adjudicatoradjudicatorconflict", + "view adjudicator-adjudicator conflicts", + ), + ( + "edit.adjudicatoradjudicatorconflict", + "edit adjudicator-adjudicator conflicts", + ), + ( + "view.adjudicatorinstitutionconflict", + "view adjudicator-institution conflicts", + ), + ( + "edit.adjudicatorinstitutionconflict", + "edit adjudicator-institution conflicts", + ), + ( + "view.teaminstitutionconflict", + "view team-institution conflicts", + ), + ( + "edit.teaminstitutionconflict", + "edit team-institution conflicts", + ), + ("view.actionlogentry", "view action log entries"), + ("view.team", "view teams"), + ("add.team", "add teams"), + ("view.teamname", "view decoded team names"), + ( + "view.anonymous", + "View names of anonymized participants", + ), + ("view.adj", "view adjudicators"), + ("add.adj", "add adjudicators"), + ("view.room", "view rooms"), + ("add.room", "add rooms"), + ("view.inst", "view institutions"), + ("add.inst", "add institutions"), + ("view.particpants", "view participants"), + ( + "view.participants.gender", + "view participants' gender information", + ), + ( + "view.participants.contact", + "view participants' contact information", + ), + ( + "view.participants.decoded", + "view participants' real names", + ), + ( + "view.participants.inst", + "view participants' institution", + ), + ( + "view.roundavailability.team", + "view round availabilities for teams", + ), + ( + "view.roundavailability.adjudicator", + "view round availabilities for adjudicators", + ), + ( + "view.roundavailability.venue", + "view round availabilities for rooms", + ), + ( + "edit.roundavailability.team", + "edit round availabilities for teams", + ), + ( + "edit.roundavailability.adjudicator", + "edit round availabilities for adjudicators", + ), + ( + "edit.roundavailability.venue", + "edit round availabilities for rooms", + ), + ("view.roundavailability", "view round availabilities"), + ("edit.roundavailability", "edit round availabilities"), + ("view.roomconstraints", "view room constraints"), + ("view.roomcategories", "view room categories"), + ("edit.roomconstraints", "edit room constraints"), + ("edit.roomcategories", "edit room categories"), + ("view.debate", "view debates (draw)"), + ("view.debate.admin", "view debates (detailed draw)"), + ("generate.debate", "generate debates (draw)"), + ("edit.debateteam", "edit debate teams (pairings)"), + ( + "view.debateadjudicator", + "view debate adjudicators (allocations)", + ), + ( + "edit.debateadjudicator", + "edit debate adjudicators (allocations)", + ), + ("view.roomallocations", "view room allocations"), + ("edit.roomallocations", "edit room allocations"), + ( + "edit.allocatesides", + "edit and confirm outround team positions", + ), + ("view.ballotsubmission.new", "view confirmed ballots"), + ( + "edit.ballotsubmission.old", + "edit non-confirmed ballots", + ), + ("view.ballotsubmission", "view any ballot"), + ("edit.ballotsubmission", "edit any ballot"), + ("add.ballotsubmission", "create ballots"), + ("mark.ballotsubmission", "confirm/discard any ballot"), + ( + "mark.ballotsubmission.others", + "confirm/discard others' ballots", + ), + ("view.ballotsubmission.graph", "view ballot graph"), + ("view.results", "view results entry page"), + ("view.roundmotion", "view motion per round"), + ("edit.roundmotion", "edit motion per round"), + ("release.draw", "release draw to public"), + ("release.motion", "release motion to public"), + ("unrelease.draw", "unrelease draw to public"), + ("unrelease.motion", "unrelease motion to public"), + ("edit.starttime", "add debate start time"), + ("view.draw", "view draws"), + ( + "view.briefingdraw", + "view draws (for the briefing room)", + ), + ( + "display.motion", + "display motion (for the briefing room)", + ), + ( + "view.tournamentpreferencemodel", + "view tournament configuration", + ), + ( + "edit.tournamentpreferencemodel", + "edit tournament configuration", + ), + ( + "view.preformedpanels", + "view existing preformed panels", + ), + ("edit.preformedpanels", "edit preformed panels"), + ( + "view.standingsoverview", + "view the overviews of standings", + ), + ( + "view.teamstandings", + "view the most recent team standings", + ), + ( + "view.speakersstandings", + "view the most recent speaker standings", + ), + ( + "view.repliesstandings", + "view the most recent replies standings", + ), + ("view.motionstab", "view the most recent motions tab"), + ("view.diversitytab", "view the diversity tab"), + ( + "view.feedbackoverview", + "view overview of judge feedback", + ), + ("edit.judgescoresbulk", "bulk update judge scores"), + ("edit.judgescoresind", "edit base scores of judges"), + ("view.feedback", "view feedback"), + ("edit.feedbackignore", "toggle ignore feedback"), + ("edit.feedbackconfirm", "toggle confirm feedback"), + ( + "view.feedbackunsubmitted", + "view feedback unsubmitted tab", + ), + ("add.feedback", "add feedback"), + ("view.adj.break", "view adjudicator break"), + ("edit.adj.break", "edit adjudicator break"), + ("edit.feedbackquestion", "edit feedback questions"), + ("edit.breakeligibility", "edit break eligibility"), + ("view.breakeligibility", "view break eligibility"), + ("edit.breakcategories", "edit break categories"), + ("view.breakcategories", "view break categories"), + ("view.speakercategories", "view speaker categories"), + ("edit.speakercategories", "edit speaker categories"), + ("view.speakereligibility", "view speaker eligibility"), + ("edit.speakereligibility", "edit speaker eligibility"), + ("view.break.overview", "view break overview"), + ("view.break", "view breaks"), + ("generate.break", "generate all breaks"), + ("view.privateurls", "view private urls"), + ( + "view.privateurls.emaillist", + "view private urls email list", + ), + ("generate.privateurls", "generate private URLs"), + ("send.privateurls", "send private URLs"), + ("view.checkin", "view checkins"), + ( + "edit.participantcheckin", + "edit participant check-in", + ), + ("edit.roomcheckin", "edit room check-in"), + ("edit.round", "edit round attributes"), + ("delete.round", "delete rounds"), + ("add.round", "create rounds"), + ("view.emails", "view email statuses"), + ("send.emails", "send participants email messages"), + ("export.xml", "export DebateXML"), + ("view.settings", "view settings"), + ("edit.settings", "edit settings"), + ], + max_length=50, + ), + blank=True, + default=list, + size=None, + verbose_name="permissions", + ), + ), + ( + "tournament", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="tournaments.tournament", + verbose_name="tournament", + ), + ), ], options={ - 'verbose_name': 'group', - 'verbose_name_plural': 'groups', - 'unique_together': {('name', 'tournament')}, + "verbose_name": "group", + "verbose_name_plural": "groups", + "unique_together": {("name", "tournament")}, }, ), migrations.CreateModel( - name='UserPermission', + name="Membership", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('permission', models.CharField(choices=[('view.adjudicatorteamconflict', 'view adjudicator-team conflicts'), ('edit.adjudicatorteamconflict', 'edit adjudicator-team conflicts'), ('view.adjudicatoradjudicatorconflict', 'view adjudicator-adjudicator conflicts'), ('edit.adjudicatoradjudicatorconflict', 'edit adjudicator-adjudicator conflicts'), ('view.adjudicatorinstitutionconflict', 'view adjudicator-institution conflicts'), ('edit.adjudicatorinstitutionconflict', 'edit adjudicator-institution conflicts'), ('view.teaminstitutionconflict', 'view team-institution conflicts'), ('edit.teaminstitutionconflict', 'edit team-institution conflicts'), ('view.actionlogentry', 'view action log entries'), ('view.team', 'view teams'), ('add.team', 'add teams'), ('view.roundavailability.team', 'view round availabilities for teams'), ('view.roundavailability.adjudicator', 'view round availabilities for adjudicators'), ('view.roundavailability.venue', 'view round availabilities for rooms'), ('edit.roundavailability.team', 'edit round availabilities for teams'), ('edit.roundavailability.adjudicator', 'edit round availabilities for adjudicators'), ('edit.roundavailability.venue', 'edit round availabilities for rooms'), ('view.debate', 'view debates (draw)'), ('view.debate.admin', 'view debates (detailed draw)'), ('generate.debate', 'generate debates (draw)'), ('edit.debateteam', 'edit debate teams (pairings)'), ('view.debateadjudicator', 'view debate adjudicators (allocations)'), ('edit.debateadjudicator', 'edit debate adjudicators (allocations)'), ('view.ballotsubmission.new', 'view confirmed ballots'), ('edit.ballotsubmission.old', 'edit non-confirmed ballots'), ('view.ballotsubmission', 'view any ballot'), ('edit.ballotsubmission', 'edit any ballot'), ('add.ballotsubmission', 'create ballots'), ('mark.ballotsubmission', 'confirm/discard any ballot'), ('mark.ballotsubmission.others', "confirm/discard others' ballots"), ('view.ballotsubmission.graph', 'view ballot graph'), ('view.tournamentpreferencemodel', 'view tournament configuration'), ('edit.tournamentpreferencemodel', 'edit tournament configuration')], max_length=50, verbose_name='permission')), - ('tournament', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tournaments.tournament', verbose_name='tournament')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "group", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="users.group", + verbose_name="group", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + verbose_name="user", + ), + ), ], options={ - 'verbose_name': 'user permission', - 'verbose_name_plural': 'user permissions', - 'unique_together': {('user', 'permission', 'tournament')}, + "verbose_name": "group membership", + "verbose_name_plural": "group memberships", + "unique_together": {("user", "group")}, }, ), migrations.CreateModel( - name='Membership', + name="UserPermission", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.group', verbose_name='group')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "permission", + models.CharField( + choices=[ + ( + "view.adjudicatorteamconflict", + "view adjudicator-team conflicts", + ), + ( + "edit.adjudicatorteamconflict", + "edit adjudicator-team conflicts", + ), + ( + "view.adjudicatoradjudicatorconflict", + "view adjudicator-adjudicator conflicts", + ), + ( + "edit.adjudicatoradjudicatorconflict", + "edit adjudicator-adjudicator conflicts", + ), + ( + "view.adjudicatorinstitutionconflict", + "view adjudicator-institution conflicts", + ), + ( + "edit.adjudicatorinstitutionconflict", + "edit adjudicator-institution conflicts", + ), + ( + "view.teaminstitutionconflict", + "view team-institution conflicts", + ), + ( + "edit.teaminstitutionconflict", + "edit team-institution conflicts", + ), + ("view.actionlogentry", "view action log entries"), + ("view.team", "view teams"), + ("add.team", "add teams"), + ("view.teamname", "view decoded team names"), + ("view.anonymous", "View names of anonymized participants"), + ("view.adj", "view adjudicators"), + ("add.adj", "add adjudicators"), + ("view.room", "view rooms"), + ("add.room", "add rooms"), + ("view.inst", "view institutions"), + ("add.inst", "add institutions"), + ("view.particpants", "view participants"), + ( + "view.participants.gender", + "view participants' gender information", + ), + ( + "view.participants.contact", + "view participants' contact information", + ), + ( + "view.participants.decoded", + "view participants' real names", + ), + ( + "view.participants.inst", + "view participants' institution", + ), + ( + "view.roundavailability.team", + "view round availabilities for teams", + ), + ( + "view.roundavailability.adjudicator", + "view round availabilities for adjudicators", + ), + ( + "view.roundavailability.venue", + "view round availabilities for rooms", + ), + ( + "edit.roundavailability.team", + "edit round availabilities for teams", + ), + ( + "edit.roundavailability.adjudicator", + "edit round availabilities for adjudicators", + ), + ( + "edit.roundavailability.venue", + "edit round availabilities for rooms", + ), + ("view.roundavailability", "view round availabilities"), + ("edit.roundavailability", "edit round availabilities"), + ("view.roomconstraints", "view room constraints"), + ("view.roomcategories", "view room categories"), + ("edit.roomconstraints", "edit room constraints"), + ("edit.roomcategories", "edit room categories"), + ("view.debate", "view debates (draw)"), + ("view.debate.admin", "view debates (detailed draw)"), + ("generate.debate", "generate debates (draw)"), + ("edit.debateteam", "edit debate teams (pairings)"), + ( + "view.debateadjudicator", + "view debate adjudicators (allocations)", + ), + ( + "edit.debateadjudicator", + "edit debate adjudicators (allocations)", + ), + ("view.roomallocations", "view room allocations"), + ("edit.roomallocations", "edit room allocations"), + ( + "edit.allocatesides", + "edit and confirm outround team positions", + ), + ("view.ballotsubmission.new", "view confirmed ballots"), + ("edit.ballotsubmission.old", "edit non-confirmed ballots"), + ("view.ballotsubmission", "view any ballot"), + ("edit.ballotsubmission", "edit any ballot"), + ("add.ballotsubmission", "create ballots"), + ("mark.ballotsubmission", "confirm/discard any ballot"), + ( + "mark.ballotsubmission.others", + "confirm/discard others' ballots", + ), + ("view.ballotsubmission.graph", "view ballot graph"), + ("view.results", "view results entry page"), + ("view.roundmotion", "view motion per round"), + ("edit.roundmotion", "edit motion per round"), + ("release.draw", "release draw to public"), + ("release.motion", "release motion to public"), + ("unrelease.draw", "unrelease draw to public"), + ("unrelease.motion", "unrelease motion to public"), + ("edit.starttime", "add debate start time"), + ("view.draw", "view draws"), + ("view.briefingdraw", "view draws (for the briefing room)"), + ( + "display.motion", + "display motion (for the briefing room)", + ), + ( + "view.tournamentpreferencemodel", + "view tournament configuration", + ), + ( + "edit.tournamentpreferencemodel", + "edit tournament configuration", + ), + ("view.preformedpanels", "view existing preformed panels"), + ("edit.preformedpanels", "edit preformed panels"), + ( + "view.standingsoverview", + "view the overviews of standings", + ), + ( + "view.teamstandings", + "view the most recent team standings", + ), + ( + "view.speakersstandings", + "view the most recent speaker standings", + ), + ( + "view.repliesstandings", + "view the most recent replies standings", + ), + ("view.motionstab", "view the most recent motions tab"), + ("view.diversitytab", "view the diversity tab"), + ( + "view.feedbackoverview", + "view overview of judge feedback", + ), + ("edit.judgescoresbulk", "bulk update judge scores"), + ("edit.judgescoresind", "edit base scores of judges"), + ("view.feedback", "view feedback"), + ("edit.feedbackignore", "toggle ignore feedback"), + ("edit.feedbackconfirm", "toggle confirm feedback"), + ( + "view.feedbackunsubmitted", + "view feedback unsubmitted tab", + ), + ("add.feedback", "add feedback"), + ("view.adj.break", "view adjudicator break"), + ("edit.adj.break", "edit adjudicator break"), + ("edit.feedbackquestion", "edit feedback questions"), + ("edit.breakeligibility", "edit break eligibility"), + ("view.breakeligibility", "view break eligibility"), + ("edit.breakcategories", "edit break categories"), + ("view.breakcategories", "view break categories"), + ("view.speakercategories", "view speaker categories"), + ("edit.speakercategories", "edit speaker categories"), + ("view.speakereligibility", "view speaker eligibility"), + ("edit.speakereligibility", "edit speaker eligibility"), + ("view.break.overview", "view break overview"), + ("view.break", "view breaks"), + ("generate.break", "generate all breaks"), + ("view.privateurls", "view private urls"), + ( + "view.privateurls.emaillist", + "view private urls email list", + ), + ("generate.privateurls", "generate private URLs"), + ("send.privateurls", "send private URLs"), + ("view.checkin", "view checkins"), + ("edit.participantcheckin", "edit participant check-in"), + ("edit.roomcheckin", "edit room check-in"), + ("edit.round", "edit round attributes"), + ("delete.round", "delete rounds"), + ("add.round", "create rounds"), + ("view.emails", "view email statuses"), + ("send.emails", "send participants email messages"), + ("export.xml", "export DebateXML"), + ("view.settings", "view settings"), + ("edit.settings", "edit settings"), + ], + max_length=50, + verbose_name="permission", + ), + ), + ( + "tournament", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="tournaments.tournament", + verbose_name="tournament", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + verbose_name="user", + ), + ), ], options={ - 'verbose_name': 'group membership', - 'verbose_name_plural': 'group memberships', - 'unique_together': {('user', 'group')}, + "verbose_name": "user permission", + "verbose_name_plural": "user permissions", + "unique_together": {("user", "permission", "tournament")}, }, ), ] diff --git a/tabbycat/users/models.py b/tabbycat/users/models.py index acd1f85e00b..fe953eb7edb 100644 --- a/tabbycat/users/models.py +++ b/tabbycat/users/models.py @@ -1,10 +1,11 @@ from django.conf import settings +from django.core.cache import cache from django.db import models from django.utils.translation import gettext_lazy as _ from utils.fields import ChoiceArrayField -from .permissions import Permission +from .permissions import PERM_CACHE_KEY, Permission class UserPermission(models.Model): @@ -20,6 +21,14 @@ class Meta: def __str__(self): return "%s: %s (%s)" % (self.user.username, self.permission, self.tournament.slug) + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + cache.set(PERM_CACHE_KEY % (self.user_id, self.tournament.slug, str(self.permission)), True) + + def delete(self, *args, **kwargs): + cache.delete(PERM_CACHE_KEY % (self.user_id, self.tournament.slug, str(self.permission))) + return super().delete(*args, **kwargs) + class Group(models.Model): name = models.CharField(max_length=100, verbose_name=_("name")) @@ -44,3 +53,11 @@ class Meta: verbose_name = _("group membership") verbose_name_plural = _("group memberships") unique_together = [('user', 'group')] + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + cache.set_many({PERM_CACHE_KEY % (self.user_id, self.group.tournament.slug, str(perm)): True for perm in self.group.permissions}) + + def delete(self, *args, **kwargs): + cache.delete_many([PERM_CACHE_KEY % (self.user_id, self.group.tournament.slug, str(perm)) for perm in self.group.permissions]) + return super().delete(*args, **kwargs) diff --git a/tabbycat/users/permissions.py b/tabbycat/users/permissions.py index b4e5a439b97..c9921e6fbb0 100644 --- a/tabbycat/users/permissions.py +++ b/tabbycat/users/permissions.py @@ -9,6 +9,8 @@ from django.conf import settings from tournaments.models import Tournament +PERM_CACHE_KEY = "user_%d_%s_%s_permission" + class Permission(TextChoices): VIEW_ADJ_TEAM_CONFLICTS = 'view.adjudicatorteamconflict', _("view adjudicator-team conflicts") @@ -174,7 +176,7 @@ def has_permission(user: 'settings.AUTH_USER_MODEL', permission: permission_type else: user._permissions[tournament.slug] = set() - cached_perm = cache.get("user_%d_%s_%s_permission" % (user.pk, tournament.slug, str(permission))) + cached_perm = cache.get(PERM_CACHE_KEY % (user.pk, tournament.slug, str(permission))) if cached_perm is not None: if cached_perm: user._permissions[tournament.slug].add(permission) @@ -186,7 +188,7 @@ def has_permission(user: 'settings.AUTH_USER_MODEL', permission: permission_type ) if perm: user._permissions[tournament.slug].add(permission) - cache.set("user_%d_%s_%s_permission" % (user.pk, tournament.slug, str(permission)), perm) + cache.set(PERM_CACHE_KEY % (user.pk, tournament.slug, str(permission)), perm) return perm @@ -194,13 +196,12 @@ def get_permissions(user: 'settings.AUTH_USER_MODEL') -> List['Tournament']: user_perms = {} for t, groups in groupby(user.membership_set.select_related('group', 'group__tournament').order_by('group__tournament').all(), key=lambda m: m.group.tournament): tournament = user_perms.setdefault(t.id, t) + tournament.permissions = set() tournament.groups = [m.group for m in groups] - permissions = set() for g in tournament.groups: - permissions |= set(g.permissions) - setattr(tournament, 'permissions', getattr(tournament, 'permissions', set()) | permissions) + tournament.permissions |= set(g.permissions) for t, perms in groupby(user.userpermission_set.select_related('tournament').order_by('tournament').all(), key=lambda p: p.tournament): tournament = user_perms.setdefault(t.id, t) - setattr(tournament, 'permissions', getattr(tournament, 'permissions', set()) | {p.permission for p in perms}) + tournament.permissions = getattr(tournament, 'permissions', set()) | {p.permission for p in perms} return list(user_perms.values()) From 790358e7da7d18bfe332e16f1bf83de679137232 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Fri, 3 May 2024 21:46:22 -0300 Subject: [PATCH 23/27] Add ability to auto-merge and confirm ballots This commit implements a combination of the "individual ballots" and "bypass ballot confirmation" settings, so that on the final submitting adjudicator to submit a ballot, the "merge ballot" sequence is run and the resulting ballot is confirmed. If there is a discrepancy for motions, the ballot becomes "postponed" to flag the incongruence. Django 4.2 is required as the DISTINCT clause has been replaced with a filter on a window. --- tabbycat/results/forms.py | 55 ++++++++++++++++++++--------------- tabbycat/results/views.py | 61 +++++++++++++++++++++++++++++++++++---- 2 files changed, 88 insertions(+), 28 deletions(-) diff --git a/tabbycat/results/forms.py b/tabbycat/results/forms.py index 6dd34821af4..dfa5af7d8b7 100644 --- a/tabbycat/results/forms.py +++ b/tabbycat/results/forms.py @@ -1,5 +1,6 @@ import logging from itertools import product +from typing import TYPE_CHECKING from asgiref.sync import async_to_sync from channels.layers import get_channel_layer @@ -19,6 +20,9 @@ DebateResultByAdjudicator, DebateResultByAdjudicatorWithScores) from .utils import get_status_meta, side_and_position_names +if TYPE_CHECKING: + from .models import BallotSubmission + logger = logging.getLogger(__name__) @@ -96,6 +100,33 @@ class ReplyScoreField(BaseScoreField): DEFAULT_STEP_VALUE = 0.5 +def broadcast_results(ballotsub: 'BallotSubmission', debate: Debate): + t = debate.round.tournament + + # 5. Notify the Latest Results consumer (for results/overview) + if ballotsub.confirmed and debate.result_status == Debate.STATUS_CONFIRMED: + group_name = BallotResultConsumer.group_prefix + "_" + t.slug + async_to_sync(get_channel_layer().group_send)(group_name, { + "type": "send_json", + "data": ballotsub.serialize_like_actionlog, + }) + + # 6. Notify the Results Page/Ballots Status Graph + group_name = BallotStatusConsumer.group_prefix + "_" + t.slug + meta = get_status_meta(debate) + async_to_sync(get_channel_layer().group_send)(group_name, { + "type": "send_json", + "data": { + 'status': debate.result_status, + 'icon': meta[0], + 'class': meta[1], + 'sort': meta[2], + 'ballot': ballotsub.serialize(t), + 'round': debate.round_id, + }, + }) + + # ============================================================================== # Result/ballot forms # ============================================================================== @@ -179,34 +210,12 @@ def save(self): self.debate.result_status = self.cleaned_data['debate_result_status'] self.debate.save() - t = self.debate.round.tournament # Need to provide a timestamp immediately for BallotStatusConsumer # as it will broadcast before the view finishes assigning one if self.ballotsub.confirmed: self.ballotsub.confirm_timestamp = timezone.now() - # 5. Notify the Latest Results consumer (for results/overview) - if self.debate.result_status == Debate.STATUS_CONFIRMED: - group_name = BallotResultConsumer.group_prefix + "_" + t.slug - async_to_sync(get_channel_layer().group_send)(group_name, { - "type": "send_json", - "data": self.ballotsub.serialize_like_actionlog, - }) - - # 6. Notify the Results Page/Ballots Status Graph - group_name = BallotStatusConsumer.group_prefix + "_" + t.slug - meta = get_status_meta(self.debate) - async_to_sync(get_channel_layer().group_send)(group_name, { - "type": "send_json", - "data": { - 'status': self.debate.result_status, - 'icon': meta[0], - 'class': meta[1], - 'sort': meta[2], - 'ballot': self.ballotsub.serialize(t), - 'round': self.debate.round_id, - }, - }) + broadcast_results(self.ballotsub, self.debate) return self.ballotsub diff --git a/tabbycat/results/views.py b/tabbycat/results/views.py index d2f3b682cd6..fcc02b2881d 100644 --- a/tabbycat/results/views.py +++ b/tabbycat/results/views.py @@ -6,7 +6,8 @@ from django.contrib import messages from django.core.exceptions import ValidationError from django.db import ProgrammingError -from django.db.models import Count, Q +from django.db.models import Count, Q, Window +from django.db.models.functions import Rank from django.http import HttpResponseRedirect from django.shortcuts import render from django.utils import timezone @@ -20,7 +21,7 @@ from adjallocation.models import DebateAdjudicator from draw.models import Debate, DebateTeam from draw.prefetch import populate_opponents -from motions.models import RoundMotion +from motions.models import DebateTeamMotionPreference, RoundMotion from motions.utils import merge_motion_vetos, merge_motions from notifications.models import BulkNotification from options.utils import use_team_code_names, use_team_code_names_data_entry @@ -36,8 +37,8 @@ from utils.views import PostOnlyRedirectView, VueTableTemplateView from .consumers import BallotStatusConsumer -from .forms import (PerAdjudicatorBallotSetForm, PerAdjudicatorEliminationBallotSetForm, SingleBallotSetForm, - SingleEliminationBallotSetForm) +from .forms import (broadcast_results, PerAdjudicatorBallotSetForm, PerAdjudicatorEliminationBallotSetForm, + SingleBallotSetForm, SingleEliminationBallotSetForm) from .models import BallotSubmission, TeamScore from .prefetch import populate_confirmed_ballots, populate_results from .result import DebateResult, get_class_name @@ -323,6 +324,9 @@ def should_send_email_receipts(self): return self.tournament.pref('enable_ballot_receipts') and not (self.debate.round.stage == Round.Stage.ELIMINATION and self.tournament.pref('teams_in_debate') == 'bp') + def postprocess_result(self): + pass + def matchup_description(self): """This is primarily shown in messages, some of which are public. This is slightly different to its use in templates, but should match given @@ -354,6 +358,8 @@ def form_valid(self, form): self.add_success_message() self.round = form.debate.round # for LogActionMixin + self.postprocess_result() + return super().form_valid(form) def populate_objects(self, prefill=True): @@ -631,6 +637,51 @@ def get_all_ballotsubs(self): return q.filter(participant_submitter=self.ballotsub.participant_submitter) return q + def postprocess_result(self): + if self.ballotsub.single_adj and self.tournament.pref('disable_ballot_confirms'): + merged_bs = BallotSubmission( + debate=self.debate, + submitter=None, + submitter_type=BallotSubmission.Submitter.AUTOMATION, + ip_address=get_ip_address(self.request), + confirmed=True, + confirm_timestamp=timezone.now(), + ) + bses = BallotSubmission.objects.filter( + debate=self.debate, participant_submitter__isnull=False, discarded=False, single_adj=True, + ).select_related('participant_submitter').annotate(ordering=Window(Rank(), partition_by="participant_submitter", order_by="-version")).filter(ordering=1) + if len(bses) != DebateAdjudicator.objects.filter(debate=self.debate).exclude(type=DebateAdjudicator.TYPE_TRAINEE).count(): + return + populate_results(bses, self.tournament) + + # Handle result conflicts + merged_result = DebateResult(merged_bs, tournament=self.tournament) + errors = merged_result.populate_from_merge(*[b.result for b in bses]) + + if len(errors) == 0: + has_errors = False + merged_bs.save() + merged_result.save() + + bs_motions = BallotSubmission.objects.filter( + id__in=[b.id for b in bses], motion__isnull=False, + ).prefetch_related('debateteammotionpreference_set__debate_team') + try: + merge_motions(merged_bs, bs_motions) + merged_bs.save() + except ValidationError: + has_errors = True + + try: + vetos = merge_motion_vetos(merged_bs, bs_motions) + DebateTeamMotionPreference.objects.bulk_create(list(vetos.values())) + except ValidationError: + has_errors = True + + self.debate.result_status = Debate.STATUS_POSTPONED if has_errors else Debate.STATUS_CONFIRMED + self.debate.save() + broadcast_results(merged_bs, self.debate) + class OldPublicNewBallotSetByIdUrlView(SingleObjectFromTournamentMixin, BasePublicNewBallotSetView): model = Adjudicator @@ -893,7 +944,7 @@ def populate_objects(self, prefill=True): bses = BallotSubmission.objects.filter( debate=self.debate, participant_submitter__isnull=False, discarded=False, single_adj=True, - ).distinct('participant_submitter').select_related('participant_submitter').order_by('participant_submitter', '-version') + ).annotate(ordering=Window(Rank(), partition_by="participant_submitter", order_by="-version")).filter(ordering=1).select_related('participant_submitter') populate_results(bses, self.tournament) self.merged_ballots = bses From ea4c5c37fc38a7ac8b982f8efff7e27a8f8d3040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Sun, 12 May 2024 19:07:56 -0300 Subject: [PATCH 24/27] Fix ballot speaker prepopulation This commit fixes the motion vetos and speaker order not getting pre- populated from earlier ballots, due to the "filled" form kwarg not getting passed. Also made a few tweaks. --- tabbycat/results/views.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tabbycat/results/views.py b/tabbycat/results/views.py index fcc02b2881d..c68b7796aa1 100644 --- a/tabbycat/results/views.py +++ b/tabbycat/results/views.py @@ -544,6 +544,7 @@ def get_form_kwargs(self): kwargs['password'] = True kwargs['result'] = self.result kwargs['vetos'] = self.vetos + kwargs['filled'] = self.prefilled return kwargs def add_success_message(self): @@ -602,10 +603,10 @@ def populate_objects(self, prefill=True): "so you can't enter results for it. Please contact a tab room official.")) def set_speakers(self, former_ballot): - if former_ballot.speakerscore_set.exists(): - for ss in former_ballot.speakerscore_set.all(): - self.result.set_speaker(ss.debate_team.side, ss.position, ss.speaker) - self.result.set_ghost(ss.debate_team.side, ss.position, ss.ghost) + for ss in former_ballot.speakerscore_set.all(): + self.result.set_speaker(ss.debate_team.side, ss.position, ss.speaker) + self.result.set_ghost(ss.debate_team.side, ss.position, ss.ghost) + else: self.prefilled = True def set_motions(self, former_ballot): @@ -614,7 +615,7 @@ def set_motions(self, former_ballot): self.prefilled = True if self.tournament.pref('motion_vetoes_enabled'): self.vetos = {} - for dtmp in former_ballot.debateteammotionpreference_set.all(): + for dtmp in former_ballot.debateteammotionpreference_set.filter(preference=3): self.vetos[dtmp.debate_team.side] = dtmp self.vetos[dtmp.debate_team.side]._roundmotion = self.round_motions[dtmp.motion_id] self.prefilled = True From 16ce99c522de166144c99a748632868575ff5789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Sun, 12 May 2024 19:28:18 -0300 Subject: [PATCH 25/27] Fix ballot merging view error --- tabbycat/results/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tabbycat/results/views.py b/tabbycat/results/views.py index c68b7796aa1..1eb06d89e95 100644 --- a/tabbycat/results/views.py +++ b/tabbycat/results/views.py @@ -924,7 +924,7 @@ def get_form_kwargs(self): def get_form(self): form = super().get_form() for error in self.errors: - msg, t, side, pos, values = error.args + msg, t, side, pos = error.args if t == 'speaker': field = form._fieldname_speaker(side, pos) elif t == 'ghost': From af1ff67c297932eb4ec50ffb9e4bf3fd997c268c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Sun, 12 May 2024 23:24:34 -0300 Subject: [PATCH 26/27] Add emojis from 13.1 to 15.1 Closes #2340 Co-Authored-By: Peta Hillier --- tabbycat/participants/emoji.py | 81 ++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/tabbycat/participants/emoji.py b/tabbycat/participants/emoji.py index 8a6e6cab0c8..d212ad7b1f4 100644 --- a/tabbycat/participants/emoji.py +++ b/tabbycat/participants/emoji.py @@ -1693,6 +1693,87 @@ def populate_code_names_from_emoji(teams, overwrite=True): ("๐Ÿชฅ", True , "Toothbrush"), ("๐Ÿชฆ", False, "Headstone"), # potentially offensive ("๐Ÿชง", False, "Placard"), # dull + + # Unicode Version 13.1 + ("๐Ÿ˜ถโ€๐ŸŒซ", True , "Cloudy Face"), + ("๐Ÿ˜ฎโ€๐Ÿ’จ", True , "Hot Air"), + ("๐Ÿ˜ตโ€๐Ÿ’ซ", True , "Hypnotised"), + ("โคโ€๐Ÿ”ฅ", True , "Fiery Heart"), + ("โคโ€๐Ÿฉน", True , "Mending Heart"), + ("๐Ÿง”โ€โ™‚", False, "Bearded Man"), # dull + ("๐Ÿง”โ€โ™€", False, "Bearded Woman"), # potentially offensive + + # Unicode Version 14.0 + ("๐Ÿซ ", True , "Melting Face"), + ("๐Ÿซข", True , "Oops Face"), + ("๐Ÿซฃ", True , "Peekaboo"), + ("๐Ÿซก", False, "Saluting Face"), # potentially offensive + ("๐Ÿซฅ", True , "Invisible Face"), + ("๐Ÿซค", True , "Diagonal Mouth"), + ("๐Ÿฅน", True , "Grateful Face"), + ("๐Ÿซฑ", False, "Rightwards Hand"), # dull + ("๐Ÿซฒ", False, "Leftwards Hand"), # dull + ("๐Ÿซณ", False, "Palm Down Hand"), # dull + ("๐Ÿซด", False, "Palm Up Hand"), # dull + ("๐Ÿซฐ", True , "Love Gesture"), + ("๐Ÿซต", True , "YOU"), + ("๐Ÿซถ", True , "Heart Hands"), + ("๐Ÿซฆ", True , "Biting Lip"), + ("๐Ÿซ…", True , "Crowned"), + ("๐Ÿซƒ", False, "Pregnant Man"), # potentially offensive + ("๐Ÿซ„", False, "Pregnant Person"), # potentially offensive + ("๐ŸงŒ", True , "Bridgekeeper"), + ("๐Ÿชธ", True , "Coral"), + ("๐Ÿชท", True , "Lotus"), + ("๐Ÿชน", False, "Empty Nest"), # dull + ("๐Ÿชบ", True , "Unladen Swallow"), + ("๐Ÿซ˜", True , "Beans"), + ("๐Ÿซ—", False, "Leak"), # dull + ("๐Ÿซ™", False, "Jar"), # dull + ("๐Ÿ›", True , "Slide"), + ("๐Ÿ›ž", True , "Wheel"), + ("๐Ÿ›Ÿ", True , "Buoy"), + ("๐Ÿชฉ", True , "Mirror Ball"), + ("๐Ÿชซ", True , "Low Battery"), + ("๐Ÿฉผ", False, "Crutch"), # potentially offensive + ("๐Ÿฉป", True , "X-Ray"), + ("๐Ÿซง", True , "Bubbles"), + ("๐Ÿชฌ", False, "Hamsa"), # potentially offensive + ("๐Ÿชช", True , "Identification Card"), + ("๐ŸŸฐ", False, "Heavy Equals Sign"), # dull + + # Unicode Version 15.0 + ("๐Ÿซจ", True , "Car Sick Face"), + ("๐Ÿฉท", False, "Pink Heart"), # too similar to another + ("๐Ÿฉต", False, "Light Blue Heart"), # too similar to another + ("๐Ÿฉถ", False, "Grey Heart"), # too similar to another + ("๐Ÿซท", True , "No Thanks Hand"), + ("๐Ÿซธ", False, "Rightwards Pushing Hand"), # too similar to another + ("๐ŸซŽ", True , "Moose"), + ("๐Ÿซ", True , "Donkey"), + ("๐Ÿชฝ", True , "Wing"), + ("๐Ÿชฟ", True , "Honking Bird"), + ("๐Ÿชผ", True , "Jellyfish"), + ("๐Ÿชป", True , "Hyacinth"), + ("๐Ÿซš", True , "Ginger"), + ("๐Ÿซ›", True , "Pea Pod"), + ("๐Ÿชญ", True , "Folding Hand Fan"), + ("๐Ÿชฎ", True , "Hair Pick"), + ("๐Ÿช‡", True , "Maracas"), + ("๐Ÿชˆ", True , "Flute"), + ("๐Ÿชฏ", False, "Khanda"), # potentially offensive + ("๐Ÿ›œ", True , "Wireless"), + + # Unicode Version 15.1 + ("๐Ÿ™‚โ€โ†”", True , "Headshake"), + ("๐Ÿ™‚โ€โ†•", True , "Nodding Face"), + ("๐Ÿšถโ€โžก", False, "Walking"), # dull + ("๐ŸงŽโ€โžก", False, "Person Kneeling"), # dull + ("๐Ÿƒโ€โžก", False, "Person Running"), # dull + ("๐Ÿฆโ€๐Ÿ”ฅ", True , "Phoenix"), + ("๐Ÿ‹โ€๐ŸŸฉ", True , "Lime"), + ("๐Ÿ„โ€๐ŸŸซ", True , "Brown Mushroom"), + ("โ›“โ€๐Ÿ’ฅ", True , "Broken Chain"), ) # The field choices are the permissible values From f8d82b4b09cfccd58a01e73d1b1d9d8c7778b608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Thu, 30 May 2024 11:07:37 +0200 Subject: [PATCH 27/27] Create migration for emojis --- ...er_institution_unique_together_and_more.py | 22 - ...23_alter_unique_together_and_team_emoji.py | 1701 +++++++++++++++++ ...2_alter_round_unique_together_and_more.py} | 2 +- 3 files changed, 1702 insertions(+), 23 deletions(-) delete mode 100644 tabbycat/participants/migrations/0023_alter_institution_unique_together_and_more.py create mode 100644 tabbycat/participants/migrations/0023_alter_unique_together_and_team_emoji.py rename tabbycat/tournaments/migrations/{0011_alter_round_unique_together_and_more.py => 0012_alter_round_unique_together_and_more.py} (92%) diff --git a/tabbycat/participants/migrations/0023_alter_institution_unique_together_and_more.py b/tabbycat/participants/migrations/0023_alter_institution_unique_together_and_more.py deleted file mode 100644 index 52df11008d7..00000000000 --- a/tabbycat/participants/migrations/0023_alter_institution_unique_together_and_more.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 5.0.4 on 2024-05-04 21:38 - -import utils.models -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('participants', '0022_rename_team_tournament_institution_short_reference_participant_tournam_160efa_idx_and_more'), - ] - - operations = [ - migrations.AlterUniqueTogether( - name='institution', - unique_together=set(), - ), - migrations.AddConstraint( - model_name='institution', - constraint=utils.models.UniqueConstraint(fields=('name', 'code'), name='partici_institution_name__code_uniq'), - ), - ] diff --git a/tabbycat/participants/migrations/0023_alter_unique_together_and_team_emoji.py b/tabbycat/participants/migrations/0023_alter_unique_together_and_team_emoji.py new file mode 100644 index 00000000000..acc19597248 --- /dev/null +++ b/tabbycat/participants/migrations/0023_alter_unique_together_and_team_emoji.py @@ -0,0 +1,1701 @@ +# Generated by Django 5.0.4 on 2024-05-30 09:04 + +import utils.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "participants", + "0022_rename_team_tournament_institution_short_reference_participant_tournam_160efa_idx_and_more", + ), + ] + + operations = [ + migrations.AlterUniqueTogether( + name="institution", + unique_together=set(), + ), + migrations.AddConstraint( + model_name="institution", + constraint=utils.models.UniqueConstraint( + fields=("name", "code"), name="partici_institution_name__code_uniq" + ), + ), + migrations.AlterField( + model_name="team", + name="emoji", + field=models.CharField( + blank=True, + choices=[ + ("โ˜บ๏ธ", "โ˜บ๏ธ White Smiling"), + ("โ˜น", "โ˜น White Frowning"), + ("โ˜๏ธ", "โ˜๏ธ White Up Pointing Index"), + ("โœŒ๏ธ", "โœŒ๏ธ Victory Hand"), + ("โœ", "โœ Writing Hand"), + ("โค๏ธ", "โค๏ธ Heavy Black Heart"), + ("โฃ", "โฃ Heart Exclamation Mark"), + ("โ˜ ", "โ˜  Skull and Crossbones"), + ("โ™จ๏ธ", "โ™จ๏ธ Hot Springs"), + ("โœˆ๏ธ", "โœˆ๏ธ Airplane"), + ("โŒ›", "โŒ› Hourglass"), + ("โŒš", "โŒš Watch"), + ("โ™ˆ", "โ™ˆ Aries"), + ("โ™‰", "โ™‰ Taurus"), + ("โ™Š", "โ™Š Gemini"), + ("โ™‹", "โ™‹ Cancer"), + ("โ™Œ", "โ™Œ Leo"), + ("โ™", "โ™ Virgo"), + ("โ™Ž", "โ™Ž Libra"), + ("โ™", "โ™ Scorpius"), + ("โ™", "โ™ Sagittarius"), + ("โ™‘", "โ™‘ Capricorn"), + ("โ™’", "โ™’ Aquarius"), + ("โ™“", "โ™“ Pisces"), + ("โ˜€๏ธ", "โ˜€๏ธ Black Sun With Rays"), + ("โ˜๏ธ", "โ˜๏ธ Cloud"), + ("โ˜‚", "โ˜‚ Umbrella"), + ("โ„๏ธ", "โ„๏ธ Snowflake"), + ("โ˜ƒ", "โ˜ƒ Snowman"), + ("โ˜„๏ธ", "โ˜„๏ธ Comet"), + ("โ™ ๏ธ", "โ™ ๏ธ Spade Suit"), + ("โ™ฅ๏ธ", "โ™ฅ๏ธ Heart Suit"), + ("โ™ฆ๏ธ", "โ™ฆ๏ธ Diamond Suit"), + ("โ™ฃ๏ธ", "โ™ฃ๏ธ Club Suit"), + ("โ–ถ๏ธ", "โ–ถ๏ธ Black Right-Pointing Triangle"), + ("โ—€๏ธ", "โ—€๏ธ Black Left-Pointing Triangle"), + ("โ˜Ž๏ธ", "โ˜Ž๏ธ Black Telephone"), + ("โŒจ", "โŒจ Keyboard"), + ("โœ‰๏ธ", "โœ‰๏ธ Envelope"), + ("โœ๏ธ", "โœ๏ธ Pencil"), + ("โœ’๏ธ", "โœ’๏ธ Black Nib"), + ("โœ‚๏ธ", "โœ‚๏ธ Scissors"), + ("โ†—๏ธ", "โ†—๏ธ North East Arrow"), + ("โžก๏ธ", "โžก๏ธ Black Rightwards Arrow"), + ("โ†˜๏ธ", "โ†˜๏ธ South East Arrow"), + ("โ†™๏ธ", "โ†™๏ธ South West Arrow"), + ("โ†–๏ธ", "โ†–๏ธ North West Arrow"), + ("โ†•๏ธ", "โ†•๏ธ Up Down Arrow"), + ("โ†”๏ธ", "โ†”๏ธ Left Right Arrow"), + ("โ†ฉ๏ธ", "โ†ฉ๏ธ Leftwards Arrow With Hook"), + ("โ†ช๏ธ", "โ†ช๏ธ Rightwards Arrow With Hook"), + ("โœก", "โœก Star of David"), + ("โ˜ธ", "โ˜ธ Wheel of Dharma"), + ("โ˜ฏ", "โ˜ฏ Yin Yang"), + ("โœ", "โœ Latin Cross"), + ("โ˜ฆ", "โ˜ฆ Orthodox Cross"), + ("โ˜ช", "โ˜ช Star and Crescent"), + ("โ˜ฎ", "โ˜ฎ Peace Symbol"), + ("โ˜ข", "โ˜ข Radioactive Sign"), + ("โ˜ฃ", "โ˜ฃ Biohazard Sign"), + ("โ˜‘๏ธ", "โ˜‘๏ธ Ballot Box With Check"), + ("โœ”๏ธ", "โœ”๏ธ Heavy Check Mark"), + ("โœ–๏ธ", "โœ–๏ธ Heavy Multiplication X"), + ("โœณ๏ธ", "โœณ๏ธ Eight Spoked Asterisk"), + ("โœด๏ธ", "โœด๏ธ Eight Pointed Black Star"), + ("โ‡๏ธ", "โ‡๏ธ Sparkle"), + ("โ€ผ๏ธ", "โ€ผ๏ธ Double Exclamation Mark"), + ("ใ€ฐ๏ธ", "ใ€ฐ๏ธ Wavy Dash"), + ("ยฉ๏ธ", "ยฉ๏ธ Copyright Sign"), + ("ยฎ๏ธ", "ยฎ๏ธ Registered Sign"), + ("โ„ข๏ธ", "โ„ข๏ธ Trade Mark Sign"), + ("โ“‚๏ธ", "โ“‚๏ธ Capital M"), + ("ใŠ—๏ธ", "ใŠ—๏ธ Congratulations"), + ("ใŠ™๏ธ", "ใŠ™๏ธ Secret"), + ("โ–ช๏ธ", "โ–ช๏ธ Black Square"), + ("โ–ซ๏ธ", "โ–ซ๏ธ White Square"), + ("#โƒฃ๏ธ", "#โƒฃ๏ธ Keycap Number Sign"), + ("*โƒฃ", "*โƒฃ Keycap Asterisk"), + ("0โƒฃ๏ธ", "0โƒฃ๏ธ Keycap Digit Zero"), + ("1โƒฃ๏ธ", "1โƒฃ๏ธ Keycap Digit One"), + ("2โƒฃ๏ธ", "2โƒฃ๏ธ Keycap Digit Two"), + ("3โƒฃ๏ธ", "3โƒฃ๏ธ Keycap Digit Three"), + ("4โƒฃ๏ธ", "4โƒฃ๏ธ Keycap Digit Four"), + ("5โƒฃ๏ธ", "5โƒฃ๏ธ Keycap Digit Five"), + ("6โƒฃ๏ธ", "6โƒฃ๏ธ Keycap Digit Six"), + ("7โƒฃ๏ธ", "7โƒฃ๏ธ Keycap Digit Seven"), + ("8โƒฃ๏ธ", "8โƒฃ๏ธ Keycap Digit Eight"), + ("9โƒฃ๏ธ", "9โƒฃ๏ธ Keycap Digit Nine"), + ("โ‰๏ธ", "โ‰๏ธ Exclamation Question Mark"), + ("โ„น๏ธ", "โ„น๏ธ Information Source"), + ("โคด๏ธ", "โคด๏ธ Right-Curve-Up"), + ("โคต๏ธ", "โคต๏ธ Right-Curve-Down"), + ("โ™ป๏ธ", "โ™ป๏ธ Recycling"), + ("ใ€ฝ๏ธ", "ใ€ฝ๏ธ Part Alternation Mark"), + ("โ—ป๏ธ", "โ—ป๏ธ White Medium Square"), + ("โ—ผ๏ธ", "โ—ผ๏ธ Black Medium Square"), + ("โ—ฝ", "โ—ฝ White Medium Small Square"), + ("โ—พ", "โ—พ Black Medium Small Square"), + ("โ˜•", "โ˜• Hot Beverage"), + ("โš ๏ธ", "โš ๏ธ Warning Sign"), + ("โ˜”", "โ˜” Umbrella With Rain Drops"), + ("โ", "โ Eject Symbol"), + ("โฌ†๏ธ", "โฌ†๏ธ Upwards Black Arrow"), + ("โฌ‡๏ธ", "โฌ‡๏ธ Downwards Black Arrow"), + ("โฌ…๏ธ", "โฌ…๏ธ Leftwards Black Arrow"), + ("โšก", "โšก High Voltage"), + ("โ˜˜", "โ˜˜ Shamrock"), + ("โš“", "โš“ Anchor"), + ("โ™ฟ", "โ™ฟ Wheelchair Symbol"), + ("โš’", "โš’ Hammer and Pick"), + ("โš™", "โš™ Gear"), + ("โš—", "โš— Alembic"), + ("โš–", "โš– Scales"), + ("โš”", "โš” Crossed Swords"), + ("โšฐ", "โšฐ Coffin"), + ("โšฑ", "โšฑ Funeral Urn"), + ("โšœ", "โšœ Fleur-De-Lis"), + ("โš›", "โš› Atom Symbol"), + ("โšช", "โšช Medium White Circle"), + ("โšซ", "โšซ Medium Black Circle"), + ("๐Ÿ€„", "๐Ÿ€„ Mahjong Tile Red Dragon"), + ("โญ", "โญ White Medium Star"), + ("โฌ›", "โฌ› Black Square"), + ("โฌœ", "โฌœ White Square"), + ("โ›‘", "โ›‘ Rescue Hat"), + ("โ›ฐ", "โ›ฐ Mountain"), + ("โ›ช", "โ›ช Church"), + ("โ›ฒ", "โ›ฒ Fountain"), + ("โ›บ", "โ›บ Tent"), + ("โ›ฝ", "โ›ฝ Fuel Pump"), + ("โ›ต", "โ›ต Sailboat"), + ("โ›ด", "โ›ด Ferry"), + ("โ›”", "โ›” No Entry"), + ("โ›…", "โ›… Overcast"), + ("โ›ˆ", "โ›ˆ Storm"), + ("โ›ฑ", "โ›ฑ Umbrella"), + ("โ›„", "โ›„ Snowman"), + ("โšฝ", "โšฝ Soccer"), + ("โšพ", "โšพ Baseball"), + ("โ›ณ", "โ›ณ Hole in One"), + ("โ›ธ", "โ›ธ Ice Skate"), + ("โ›ท", "โ›ท Skier"), + ("โ›น", "โ›น Person With Ball"), + ("โ›", "โ› Pick"), + ("โ›“", "โ›“ Chains"), + ("โ›ฉ", "โ›ฉ Shinto Shrine"), + ("โญ•", "โญ• Heavy Large Circle"), + ("โ—", "โ— Heavy Exclamation Mark"), + ("๐Ÿ…ฟ๏ธ", "๐Ÿ…ฟ๏ธ Squared P"), + ("๐Ÿˆฏ", "๐Ÿˆฏ Squared ๆŒ‡ (Finger)"), + ("๐Ÿˆš", "๐Ÿˆš Squared CJK Unified Ideograph-7121"), + ("๐Ÿ˜", "๐Ÿ˜ Smiling Eyes"), + ("๐Ÿ˜‚", "๐Ÿ˜‚ Joy Tears"), + ("๐Ÿ˜ƒ", "๐Ÿ˜ƒ Smiling Face With Open Mouth"), + ("๐Ÿ˜„", "๐Ÿ˜„ Smiling Face With Open Mouth and Smiling Eyes"), + ("๐Ÿ˜…", "๐Ÿ˜… Cold Sweat"), + ("๐Ÿ˜†", "๐Ÿ˜† Closed Eyes"), + ("๐Ÿ˜‰", "๐Ÿ˜‰ Winky"), + ("๐Ÿ˜Š", "๐Ÿ˜Š Smiling Eyes"), + ("๐Ÿ˜‹", "๐Ÿ˜‹ Face Savouring Delicious Food"), + ("๐Ÿ˜Ž", "๐Ÿ˜Ž Shaded Eyes"), + ("๐Ÿ˜", "๐Ÿ˜ Heart Eyes"), + ("๐Ÿ˜˜", "๐Ÿ˜˜ Kissy"), + ("๐Ÿ˜š", "๐Ÿ˜š Kissing Face With Closed Eyes"), + ("๐Ÿ˜‡", "๐Ÿ˜‡ Halo"), + ("๐Ÿ˜", "๐Ÿ˜ Neutral"), + ("๐Ÿ˜ถ", "๐Ÿ˜ถ No Mouth"), + ("๐Ÿ˜", "๐Ÿ˜ Smirking"), + ("๐Ÿ˜ฃ", "๐Ÿ˜ฃ Persevering"), + ("๐Ÿ˜ฅ", "๐Ÿ˜ฅ Disappointed"), + ("๐Ÿ˜ช", "๐Ÿ˜ช Sleepy"), + ("๐Ÿ˜ซ", "๐Ÿ˜ซ Tired"), + ("๐Ÿ˜Œ", "๐Ÿ˜Œ Relieved"), + ("๐Ÿ˜œ", "๐Ÿ˜œ Tongue Out"), + ("๐Ÿ˜", "๐Ÿ˜ Tongue Out Closed Eyes"), + ("๐Ÿ˜’", "๐Ÿ˜’ Unamused"), + ("๐Ÿ˜“", "๐Ÿ˜“ Cold Sweat"), + ("๐Ÿ˜”", "๐Ÿ˜” Pensive"), + ("๐Ÿ˜–", "๐Ÿ˜– Confounded"), + ("๐Ÿ˜ท", "๐Ÿ˜ท Medical Mask"), + ("๐Ÿ˜ฒ", "๐Ÿ˜ฒ Astonished"), + ("๐Ÿ˜ž", "๐Ÿ˜ž Disappointed"), + ("๐Ÿ˜ค", "๐Ÿ˜ค Face With Look of Triumph"), + ("๐Ÿ˜ข", "๐Ÿ˜ข Crying"), + ("๐Ÿ˜ญ", "๐Ÿ˜ญ Sobbing"), + ("๐Ÿ˜จ", "๐Ÿ˜จ Fearful"), + ("๐Ÿ˜ฉ", "๐Ÿ˜ฉ Weary"), + ("๐Ÿ˜ฐ", "๐Ÿ˜ฐ Open Mouth Cold Sweat"), + ("๐Ÿ˜ฑ", "๐Ÿ˜ฑ Screaming"), + ("๐Ÿ˜ณ", "๐Ÿ˜ณ Flushed"), + ("๐Ÿ˜ต", "๐Ÿ˜ต Dizzy"), + ("๐Ÿ˜ก", "๐Ÿ˜ก Pouting"), + ("๐Ÿ˜ ", "๐Ÿ˜  Angry"), + ("๐Ÿ‘ฟ", "๐Ÿ‘ฟ Imp"), + ("๐Ÿ˜ˆ", "๐Ÿ˜ˆ Smiling Face With Horns"), + ("๐Ÿ‘ฆ", "๐Ÿ‘ฆ Boy"), + ("๐Ÿ‘ง", "๐Ÿ‘ง Girl"), + ("๐Ÿ‘จ", "๐Ÿ‘จ Generic Man"), + ("๐Ÿ‘ฉ", "๐Ÿ‘ฉ Generic Woman"), + ("๐Ÿ‘ด", "๐Ÿ‘ด Older Man"), + ("๐Ÿ‘ต", "๐Ÿ‘ต Older Woman"), + ("๐Ÿ‘ถ", "๐Ÿ‘ถ Baby"), + ("๐Ÿ‘ฑ", "๐Ÿ‘ฑ Person With Blond Hair"), + ("๐Ÿ‘ฎ", "๐Ÿ‘ฎ Police Officer"), + ("๐Ÿ‘ฒ", "๐Ÿ‘ฒ Man With Gua Pi Mao"), + ("๐Ÿ‘ณ", "๐Ÿ‘ณ Man With Turban"), + ("๐Ÿ‘ท", "๐Ÿ‘ท Trade Worker"), + ("๐Ÿ‘ธ", "๐Ÿ‘ธ Princess"), + ("๐Ÿ’‚", "๐Ÿ’‚ Guardsman"), + ("๐ŸŽ…", "๐ŸŽ… Santa Claus"), + ("๐Ÿ‘ผ", "๐Ÿ‘ผ Baby Angel"), + ("๐Ÿ‘ฏ", "๐Ÿ‘ฏ Bunny Women"), + ("๐Ÿ’†", "๐Ÿ’† Face Massage"), + ("๐Ÿ’‡", "๐Ÿ’‡ Haircut"), + ("๐Ÿ‘ฐ", "๐Ÿ‘ฐ Bride"), + ("๐Ÿ™", "๐Ÿ™ Person Frowning"), + ("๐Ÿ™Ž", "๐Ÿ™Ž Person With Pouting"), + ("๐Ÿ™…", "๐Ÿ™… Block Gesture"), + ("๐Ÿ™†", "๐Ÿ™† OK Gesture"), + ("๐Ÿ’", "๐Ÿ’ Sass Gesture"), + ("๐Ÿ™‹", "๐Ÿ™‹ Raised Hand"), + ("๐Ÿ™‡", "๐Ÿ™‡ Deep Bow"), + ("๐Ÿ™Œ", "๐Ÿ™Œ Praise Hands"), + ("๐Ÿ™", "๐Ÿ™ Prayer Hands"), + ("๐Ÿ‘ค", "๐Ÿ‘ค Bust in Silhouette"), + ("๐Ÿ‘ฅ", "๐Ÿ‘ฅ Busts in Silhouette"), + ("๐Ÿšถ", "๐Ÿšถ Pedestrian"), + ("๐Ÿƒ", "๐Ÿƒ Runner"), + ("๐Ÿ’ƒ", "๐Ÿ’ƒ Dancer"), + ("๐Ÿ’", "๐Ÿ’ Kiss"), + ("๐Ÿ’‘", "๐Ÿ’‘ Heteronormative Couple"), + ("๐Ÿ‘ช", "๐Ÿ‘ช Hetero Family"), + ("๐Ÿ‘ซ", "๐Ÿ‘ซ Man & Woman"), + ("๐Ÿ‘ฌ", "๐Ÿ‘ฌ Two Men"), + ("๐Ÿ‘ญ", "๐Ÿ‘ญ Two Women"), + ("๐Ÿ’ช", "๐Ÿ’ช Biceps"), + ("๐Ÿ‘ˆ", "๐Ÿ‘ˆ Left Pointing Backhand"), + ("๐Ÿ‘‰", "๐Ÿ‘‰ Right Pointing Backhand"), + ("๐Ÿ‘†", "๐Ÿ‘† Pointing Hand"), + ("๐Ÿ‘‡", "๐Ÿ‘‡ Down Pointing Backhand"), + ("โœŠ", "โœŠ Power Hand"), + ("โœ‹", "โœ‹ Palm Hand"), + ("๐Ÿ‘Š", "๐Ÿ‘Š Fist Hand"), + ("๐Ÿ‘Œ", "๐Ÿ‘Œ OK Hand"), + ("๐Ÿ‘", "๐Ÿ‘ Thumbs Up"), + ("๐Ÿ‘Ž", "๐Ÿ‘Ž Thumbs Down"), + ("๐Ÿ‘‹", "๐Ÿ‘‹ Waving Hand Sign"), + ("๐Ÿ‘", "๐Ÿ‘ Clappy Hands"), + ("๐Ÿ‘", "๐Ÿ‘ Open Hands Sign"), + ("๐Ÿ’…", "๐Ÿ’… Nail Polish"), + ("๐Ÿ‘ฃ", "๐Ÿ‘ฃ Footprints"), + ("๐Ÿ‘€", "๐Ÿ‘€ Eyes"), + ("๐Ÿ‘‚", "๐Ÿ‘‚ Ear"), + ("๐Ÿ‘ƒ", "๐Ÿ‘ƒ Nose"), + ("๐Ÿ‘…", "๐Ÿ‘… Lick"), + ("๐Ÿ‘„", "๐Ÿ‘„ Mouth"), + ("๐Ÿ’‹", "๐Ÿ’‹ Kiss Mark"), + ("๐Ÿ’˜", "๐Ÿ’˜ Cupid Arrow"), + ("๐Ÿ’“", "๐Ÿ’“ Beating Heart"), + ("๐Ÿ’”", "๐Ÿ’” Broken Heart"), + ("๐Ÿ’•", "๐Ÿ’• Two Hearts"), + ("๐Ÿ’–", "๐Ÿ’– Sparkly Heart"), + ("๐Ÿ’—", "๐Ÿ’— Growing Heart"), + ("๐Ÿ’™", "๐Ÿ’™ Blue Heart"), + ("๐Ÿ’š", "๐Ÿ’š Green Heart"), + ("๐Ÿ’›", "๐Ÿ’› Yellow Heart"), + ("๐Ÿ’œ", "๐Ÿ’œ Purple Heart"), + ("๐Ÿ’", "๐Ÿ’ Heart With Ribbon"), + ("๐Ÿ’ž", "๐Ÿ’ž Revolving Hearts"), + ("๐Ÿ’Ÿ", "๐Ÿ’Ÿ Heart Decoration"), + ("๐Ÿ’Œ", "๐Ÿ’Œ Love Letter"), + ("๐Ÿ’ง", "๐Ÿ’ง Droplet"), + ("๐Ÿ’ค", "๐Ÿ’ค ZZZ"), + ("๐Ÿ’ข", "๐Ÿ’ข Anger"), + ("๐Ÿ’ฃ", "๐Ÿ’ฃ Bomb"), + ("๐Ÿ’ฅ", "๐Ÿ’ฅ Sparks"), + ("๐Ÿ’ฆ", "๐Ÿ’ฆ Splashing"), + ("๐Ÿ’จ", "๐Ÿ’จ Dash"), + ("๐Ÿ’ซ", "๐Ÿ’ซ Shooting Star"), + ("๐Ÿ’ฌ", "๐Ÿ’ฌ Speech Bubble"), + ("๐Ÿ’ญ", "๐Ÿ’ญ Thinky Cloud"), + ("๐Ÿ‘“", "๐Ÿ‘“ Eyeglasses"), + ("๐Ÿ‘”", "๐Ÿ‘” Business Casual"), + ("๐Ÿ‘•", "๐Ÿ‘• T-Shirt"), + ("๐Ÿ‘–", "๐Ÿ‘– Jeans"), + ("๐Ÿ‘—", "๐Ÿ‘— Dress"), + ("๐Ÿ‘˜", "๐Ÿ‘˜ Kimono"), + ("๐Ÿ‘™", "๐Ÿ‘™ Bikini"), + ("๐Ÿ‘š", "๐Ÿ‘š Womans Clothes"), + ("๐Ÿ‘›", "๐Ÿ‘› Purse"), + ("๐Ÿ‘œ", "๐Ÿ‘œ Handbag"), + ("๐Ÿ‘", "๐Ÿ‘ Pouch"), + ("๐ŸŽ’", "๐ŸŽ’ Backpack"), + ("๐Ÿ‘ž", "๐Ÿ‘ž Mans Shoe"), + ("๐Ÿ‘Ÿ", "๐Ÿ‘Ÿ Running Shoe"), + ("๐Ÿ‘ ", "๐Ÿ‘  Heels"), + ("๐Ÿ‘ก", "๐Ÿ‘ก Womans Sandal"), + ("๐Ÿ‘ข", "๐Ÿ‘ข Womans Boots"), + ("๐Ÿ‘‘", "๐Ÿ‘‘ Crown"), + ("๐Ÿ‘’", "๐Ÿ‘’ Lady's Hat"), + ("๐ŸŽฉ", "๐ŸŽฉ Top Hat"), + ("๐Ÿ’„", "๐Ÿ’„ Lipstick"), + ("๐Ÿ’", "๐Ÿ’ Proposal"), + ("๐Ÿ’Ž", "๐Ÿ’Ž Gem"), + ("๐Ÿ‘น", "๐Ÿ‘น Japanese Ogre"), + ("๐Ÿ‘บ", "๐Ÿ‘บ Japanese Goblin"), + ("๐Ÿ‘ป", "๐Ÿ‘ป Ghost"), + ("๐Ÿ’€", "๐Ÿ’€ Skull"), + ("๐Ÿ‘ฝ", "๐Ÿ‘ฝ Alien"), + ("๐Ÿ‘พ", "๐Ÿ‘พ Space Invader"), + ("๐Ÿ’ฉ", "๐Ÿ’ฉ Pile of Poo"), + ("๐Ÿต", "๐Ÿต Monkey"), + ("๐Ÿ™ˆ", "๐Ÿ™ˆ See No Evil"), + ("๐Ÿ™‰", "๐Ÿ™‰ Hear No Evil"), + ("๐Ÿ™Š", "๐Ÿ™Š Speak No Evil"), + ("๐Ÿ’", "๐Ÿ’ Monkey"), + ("๐Ÿถ", "๐Ÿถ Dog"), + ("๐Ÿ•", "๐Ÿ• Dog"), + ("๐Ÿฉ", "๐Ÿฉ Poodle"), + ("๐Ÿบ", "๐Ÿบ Wolf"), + ("๐Ÿฑ", "๐Ÿฑ Cat"), + ("๐Ÿ˜ธ", "๐Ÿ˜ธ Grinning Cat with Smiling Eyes"), + ("๐Ÿ˜น", "๐Ÿ˜น Cat with Tears of Joy"), + ("๐Ÿ˜บ", "๐Ÿ˜บ Smiling Cat with Open Mouth"), + ("๐Ÿ˜ป", "๐Ÿ˜ป Smiling Cat with Heart Eyes"), + ("๐Ÿ˜ผ", "๐Ÿ˜ผ Cat with Wry Smile"), + ("๐Ÿ˜ฝ", "๐Ÿ˜ฝ Kissing Cat with Closed Eyes"), + ("๐Ÿ˜พ", "๐Ÿ˜พ Pouting Cat Face"), + ("๐Ÿ˜ฟ", "๐Ÿ˜ฟ Crying Cat Face"), + ("๐Ÿ™€", "๐Ÿ™€ Weary Cat Face"), + ("๐Ÿˆ", "๐Ÿˆ Cat"), + ("๐Ÿฏ", "๐Ÿฏ Tiger"), + ("๐Ÿ…", "๐Ÿ… Tiger"), + ("๐Ÿ†", "๐Ÿ† Leopard"), + ("๐Ÿด", "๐Ÿด Horse"), + ("๐ŸŽ", "๐ŸŽ Horse"), + ("๐Ÿฎ", "๐Ÿฎ Cow"), + ("๐Ÿ‚", "๐Ÿ‚ Ox"), + ("๐Ÿƒ", "๐Ÿƒ Water Buffalo"), + ("๐Ÿ„", "๐Ÿ„ Cow"), + ("๐Ÿท", "๐Ÿท Pig"), + ("๐Ÿ–", "๐Ÿ– Pig"), + ("๐Ÿ—", "๐Ÿ— Boar"), + ("๐Ÿฝ", "๐Ÿฝ Pig Nose"), + ("๐Ÿ", "๐Ÿ Ram"), + ("๐Ÿ‘", "๐Ÿ‘ Sheep"), + ("๐Ÿ", "๐Ÿ Goat"), + ("๐Ÿช", "๐Ÿช Dromedary Camel"), + ("๐Ÿซ", "๐Ÿซ Bactrian Camel"), + ("๐Ÿ˜", "๐Ÿ˜ Elephant"), + ("๐Ÿญ", "๐Ÿญ Mouse"), + ("๐Ÿ", "๐Ÿ Mouse"), + ("๐Ÿ€", "๐Ÿ€ Rat"), + ("๐Ÿน", "๐Ÿน Hamster"), + ("๐Ÿฐ", "๐Ÿฐ Rabbit"), + ("๐Ÿ‡", "๐Ÿ‡ Rabbit"), + ("๐Ÿป", "๐Ÿป Bear"), + ("๐Ÿจ", "๐Ÿจ Koala"), + ("๐Ÿผ", "๐Ÿผ Panda"), + ("๐Ÿพ", "๐Ÿพ Paw Prints"), + ("๐Ÿ”", "๐Ÿ” Chicken"), + ("๐Ÿ“", "๐Ÿ“ Rooster"), + ("๐Ÿฃ", "๐Ÿฃ Hatching"), + ("๐Ÿค", "๐Ÿค Chick"), + ("๐Ÿฅ", "๐Ÿฅ Front-Facing Baby Chick"), + ("๐Ÿฆ", "๐Ÿฆ Bird"), + ("๐Ÿง", "๐Ÿง Penguin"), + ("๐Ÿธ", "๐Ÿธ Frog"), + ("๐ŸŠ", "๐ŸŠ Croc"), + ("๐Ÿข", "๐Ÿข Turtle"), + ("๐Ÿ", "๐Ÿ Slithering"), + ("๐Ÿฒ", "๐Ÿฒ Dragon"), + ("๐Ÿ‰", "๐Ÿ‰ Dragon"), + ("๐Ÿณ", "๐Ÿณ Whale"), + ("๐Ÿ‹", "๐Ÿ‹ Whale"), + ("๐Ÿฌ", "๐Ÿฌ Dolphin"), + ("๐ŸŸ", "๐ŸŸ Fish"), + ("๐Ÿ ", "๐Ÿ  Fish"), + ("๐Ÿก", "๐Ÿก Blowfish"), + ("๐Ÿ™", "๐Ÿ™ Octopus"), + ("๐Ÿš", "๐Ÿš Shell"), + ("๐ŸŒ", "๐ŸŒ Snail"), + ("๐Ÿ›", "๐Ÿ› Bug"), + ("๐Ÿœ", "๐Ÿœ Ant"), + ("๐Ÿ", "๐Ÿ Honeybee"), + ("๐Ÿž", "๐Ÿž Lady Beetle"), + ("๐Ÿ’", "๐Ÿ’ Bouquet"), + ("๐ŸŒธ", "๐ŸŒธ Sakura"), + ("๐Ÿ’ฎ", "๐Ÿ’ฎ White Flower"), + ("๐ŸŒน", "๐ŸŒน Rose"), + ("๐ŸŒบ", "๐ŸŒบ Hibiscus"), + ("๐ŸŒป", "๐ŸŒป Sunflower"), + ("๐ŸŒผ", "๐ŸŒผ Blossom"), + ("๐ŸŒท", "๐ŸŒท Tulip"), + ("๐ŸŒฑ", "๐ŸŒฑ Seedling"), + ("๐ŸŒฒ", "๐ŸŒฒ Evergreen Tree"), + ("๐ŸŒณ", "๐ŸŒณ Deciduous Tree"), + ("๐ŸŒด", "๐ŸŒด Palm Tree"), + ("๐ŸŒต", "๐ŸŒต Cactus"), + ("๐ŸŒพ", "๐ŸŒพ Ear of Rice"), + ("๐ŸŒฟ", "๐ŸŒฟ Herb"), + ("๐Ÿ€", "๐Ÿ€ Clover"), + ("๐Ÿ", "๐Ÿ Maple Leaf"), + ("๐Ÿ‚", "๐Ÿ‚ Fallen Leaf"), + ("๐Ÿƒ", "๐Ÿƒ Blown Leaves"), + ("๐Ÿ‡", "๐Ÿ‡ Grapes"), + ("๐Ÿˆ", "๐Ÿˆ Melon"), + ("๐Ÿ‰", "๐Ÿ‰ Watermelon"), + ("๐ŸŠ", "๐ŸŠ Tangerine"), + ("๐Ÿ‹", "๐Ÿ‹ Lemon"), + ("๐ŸŒ", "๐ŸŒ Banana"), + ("๐Ÿ", "๐Ÿ Pineapple"), + ("๐ŸŽ", "๐ŸŽ Red Apple"), + ("๐Ÿ", "๐Ÿ Green Apple"), + ("๐Ÿ", "๐Ÿ Pear"), + ("๐Ÿ‘", "๐Ÿ‘ Peach"), + ("๐Ÿ’", "๐Ÿ’ Cherries"), + ("๐Ÿ“", "๐Ÿ“ Strawberry"), + ("๐Ÿ…", "๐Ÿ… Tomato"), + ("๐Ÿ†", "๐Ÿ† Eggplant"), + ("๐ŸŒฝ", "๐ŸŒฝ Corn"), + ("๐Ÿ„", "๐Ÿ„ Mushroom"), + ("๐ŸŒฐ", "๐ŸŒฐ Chestnut"), + ("๐Ÿž", "๐Ÿž Bread"), + ("๐Ÿ–", "๐Ÿ– Meat on Bone"), + ("๐Ÿ—", "๐Ÿ— Poultry Leg"), + ("๐Ÿ”", "๐Ÿ” Hamburger"), + ("๐ŸŸ", "๐ŸŸ Fries"), + ("๐Ÿ•", "๐Ÿ• Pizza"), + ("๐Ÿฒ", "๐Ÿฒ Pot of Food"), + ("๐Ÿฑ", "๐Ÿฑ Bento Box"), + ("๐Ÿ˜", "๐Ÿ˜ Rice Cracker"), + ("๐Ÿ™", "๐Ÿ™ Rice Ball"), + ("๐Ÿš", "๐Ÿš Cooked Rice"), + ("๐Ÿ›", "๐Ÿ› Curry and Rice"), + ("๐Ÿœ", "๐Ÿœ Steaming Bowl"), + ("๐Ÿ", "๐Ÿ Spaghetti"), + ("๐Ÿ ", "๐Ÿ  Sweet Potato"), + ("๐Ÿข", "๐Ÿข Oden"), + ("๐Ÿฃ", "๐Ÿฃ Sushi"), + ("๐Ÿค", "๐Ÿค Fried Shrimp"), + ("๐Ÿฅ", "๐Ÿฅ Fish Cake With Swirl Design"), + ("๐Ÿก", "๐Ÿก Dango"), + ("๐Ÿฆ", "๐Ÿฆ Ice Cream"), + ("๐Ÿง", "๐Ÿง Shaved Ice"), + ("๐Ÿจ", "๐Ÿจ Ice Cream"), + ("๐Ÿฉ", "๐Ÿฉ Doughnut"), + ("๐Ÿช", "๐Ÿช Cookie"), + ("๐ŸŽ‚", "๐ŸŽ‚ Birthday Cake"), + ("๐Ÿฐ", "๐Ÿฐ Shortcake"), + ("๐Ÿซ", "๐Ÿซ Chocolate Bar"), + ("๐Ÿฌ", "๐Ÿฌ Candy"), + ("๐Ÿญ", "๐Ÿญ Lollipop"), + ("๐Ÿฎ", "๐Ÿฎ Custard"), + ("๐Ÿฏ", "๐Ÿฏ Honey Pot"), + ("๐Ÿผ", "๐Ÿผ Baby Bottle"), + ("๐Ÿต", "๐Ÿต Teacup Without Handle"), + ("๐Ÿถ", "๐Ÿถ Sake Bottle and Cup"), + ("๐Ÿท", "๐Ÿท Wine Glass"), + ("๐Ÿธ", "๐Ÿธ Cocktail Glass"), + ("๐Ÿน", "๐Ÿน Tropical Drink"), + ("๐Ÿบ", "๐Ÿบ Beer"), + ("๐Ÿป", "๐Ÿป Clinking Beer Mugs"), + ("๐Ÿด", "๐Ÿด Fork & Knife"), + ("๐Ÿณ", "๐Ÿณ Cooking"), + ("๐ŸŒ", "๐ŸŒ Earth Globe Europe-Africa"), + ("๐ŸŒŽ", "๐ŸŒŽ Earth Globe Americas"), + ("๐ŸŒ", "๐ŸŒ Earth Globe Asia-Australia"), + ("๐ŸŒ", "๐ŸŒ Globe With Meridians"), + ("๐ŸŒ‹", "๐ŸŒ‹ Volcano"), + ("๐Ÿ—ป", "๐Ÿ—ป Mount Fuji"), + ("๐Ÿ ", "๐Ÿ  House"), + ("๐Ÿก", "๐Ÿก House With Garden"), + ("๐Ÿข", "๐Ÿข Office"), + ("๐Ÿฃ", "๐Ÿฃ Japanese Post Office"), + ("๐Ÿค", "๐Ÿค European Post Office"), + ("๐Ÿฅ", "๐Ÿฅ Hospital"), + ("๐Ÿฆ", "๐Ÿฆ Bank"), + ("๐Ÿจ", "๐Ÿจ Hotel"), + ("๐Ÿฉ", "๐Ÿฉ Love Hotel"), + ("๐Ÿช", "๐Ÿช Convenience Store"), + ("๐Ÿซ", "๐Ÿซ School"), + ("๐Ÿฌ", "๐Ÿฌ Department Store"), + ("๐Ÿญ", "๐Ÿญ Factory"), + ("๐Ÿฏ", "๐Ÿฏ Japanese Castle"), + ("๐Ÿฐ", "๐Ÿฐ Castle"), + ("๐Ÿ’’", "๐Ÿ’’ Wedding"), + ("๐Ÿ—ผ", "๐Ÿ—ผ Tokyo Tower"), + ("๐Ÿ—ฝ", "๐Ÿ—ฝ Liberty"), + ("๐Ÿ—พ", "๐Ÿ—พ Silhouette of Japan"), + ("๐ŸŒ", "๐ŸŒ Foggy"), + ("๐ŸŒƒ", "๐ŸŒƒ Night With Stars"), + ("๐ŸŒ„", "๐ŸŒ„ Sunrise Over Mountains"), + ("๐ŸŒ…", "๐ŸŒ… Sunrise"), + ("๐ŸŒ†", "๐ŸŒ† Cityscape at Dusk"), + ("๐ŸŒ‡", "๐ŸŒ‡ Sunset Over Buildings"), + ("๐ŸŒ‰", "๐ŸŒ‰ Bridge at Night"), + ("๐ŸŒŠ", "๐ŸŒŠ Big Wave"), + ("๐Ÿ—ฟ", "๐Ÿ—ฟ Moyai"), + ("๐ŸŒŒ", "๐ŸŒŒ Milky Way"), + ("๐ŸŽ ", "๐ŸŽ  Carousel Horse"), + ("๐ŸŽก", "๐ŸŽก Ferris Wheel"), + ("๐ŸŽข", "๐ŸŽข Roller Coaster"), + ("๐Ÿ’ˆ", "๐Ÿ’ˆ Barber Pole"), + ("๐ŸŽช", "๐ŸŽช Circus Tent"), + ("๐ŸŽญ", "๐ŸŽญ Performing Arts"), + ("๐ŸŽจ", "๐ŸŽจ Palette"), + ("๐ŸŽฐ", "๐ŸŽฐ Slot Machine"), + ("๐Ÿš‚", "๐Ÿš‚ Steam Locomotive"), + ("๐Ÿšƒ", "๐Ÿšƒ Railcar"), + ("๐Ÿš„", "๐Ÿš„ Fast Train"), + ("๐Ÿš…", "๐Ÿš… Fast Train with Bullet Nose"), + ("๐Ÿš†", "๐Ÿš† Train"), + ("๐Ÿš‡", "๐Ÿš‡ Metro"), + ("๐Ÿšˆ", "๐Ÿšˆ Light Rail"), + ("๐Ÿš‰", "๐Ÿš‰ Station"), + ("๐ŸšŠ", "๐ŸšŠ Tram"), + ("๐Ÿš", "๐Ÿš Monorail"), + ("๐Ÿšž", "๐Ÿšž Mountain Railway"), + ("๐Ÿš‹", "๐Ÿš‹ Tram Car"), + ("๐ŸšŒ", "๐ŸšŒ Bus"), + ("๐Ÿš", "๐Ÿš Bus"), + ("๐ŸšŽ", "๐ŸšŽ Trolleybus"), + ("๐Ÿš", "๐Ÿš Bus Stop"), + ("๐Ÿš", "๐Ÿš Minibus"), + ("๐Ÿš‘", "๐Ÿš‘ Ambulance"), + ("๐Ÿš’", "๐Ÿš’ Fire Engine"), + ("๐Ÿš“", "๐Ÿš“ Police Car"), + ("๐Ÿš”", "๐Ÿš” Police Car"), + ("๐Ÿš•", "๐Ÿš• Taxi"), + ("๐Ÿš–", "๐Ÿš– Oncoming Taxi"), + ("๐Ÿš—", "๐Ÿš— Automobile"), + ("๐Ÿš˜", "๐Ÿš˜ Automobile"), + ("๐Ÿš™", "๐Ÿš™ Recreational Vehicle"), + ("๐Ÿšš", "๐Ÿšš Truck"), + ("๐Ÿš›", "๐Ÿš› Articulated Lorry"), + ("๐Ÿšœ", "๐Ÿšœ Tractor"), + ("๐Ÿšฒ", "๐Ÿšฒ Bicycle"), + ("๐Ÿšณ", "๐Ÿšณ No Bicycles"), + ("๐Ÿšจ", "๐Ÿšจ Alert Light"), + ("๐Ÿ”ฑ", "๐Ÿ”ฑ Trident"), + ("๐Ÿšฃ", "๐Ÿšฃ Rowboat"), + ("๐Ÿšค", "๐Ÿšค Speedboat"), + ("๐Ÿšข", "๐Ÿšข Ship"), + ("๐Ÿ’บ", "๐Ÿ’บ Seat"), + ("๐Ÿš", "๐Ÿš Helicopter"), + ("๐ŸšŸ", "๐ŸšŸ Suspension Railway"), + ("๐Ÿš ", "๐Ÿš  Sky Tram"), + ("๐Ÿšก", "๐Ÿšก Aerial Tramway"), + ("๐Ÿš€", "๐Ÿš€ Rocket"), + ("๐Ÿง", "๐Ÿง ATM"), + ("๐Ÿšฎ", "๐Ÿšฎ Put Litter in Its Place"), + ("๐Ÿšฅ", "๐Ÿšฅ Horizontal Traffic Light"), + ("๐Ÿšฆ", "๐Ÿšฆ Traffic Light"), + ("๐Ÿšง", "๐Ÿšง Hazard Sign"), + ("๐Ÿšซ", "๐Ÿšซ Prohibited"), + ("๐Ÿšญ", "๐Ÿšญ No Smoking"), + ("๐Ÿšฏ", "๐Ÿšฏ Do Not Litter"), + ("๐Ÿšฐ", "๐Ÿšฐ Tap Water"), + ("๐Ÿšฑ", "๐Ÿšฑ Non-Potable Water"), + ("๐Ÿšท", "๐Ÿšท No Pedestrians"), + ("๐Ÿšธ", "๐Ÿšธ Children Crossing"), + ("๐Ÿšน", "๐Ÿšน Mens Symbol"), + ("๐Ÿšบ", "๐Ÿšบ Womens Symbol"), + ("๐Ÿšป", "๐Ÿšป Restroom"), + ("๐Ÿšผ", "๐Ÿšผ Baby Symbol"), + ("๐Ÿšพ", "๐Ÿšพ Water Closet"), + ("๐Ÿ›‚", "๐Ÿ›‚ Passport Control"), + ("๐Ÿ›ƒ", "๐Ÿ›ƒ Customs"), + ("๐Ÿ›„", "๐Ÿ›„ Baggage Claim"), + ("๐Ÿ›…", "๐Ÿ›… Left Luggage"), + ("๐Ÿšช", "๐Ÿšช Door"), + ("๐Ÿšฝ", "๐Ÿšฝ Toilet"), + ("๐Ÿšฟ", "๐Ÿšฟ Shower"), + ("๐Ÿ›€", "๐Ÿ›€ Bath"), + ("๐Ÿ›", "๐Ÿ› Bathtub"), + ("โณ", "โณ Hourglass"), + ("โฐ", "โฐ Alarm Clock"), + ("โฑ", "โฑ Stopwatch"), + ("โฒ", "โฒ Timer Clock"), + ("๐Ÿ•›", "๐Ÿ•› Twelve O'Clock"), + ("๐Ÿ•ง", "๐Ÿ•ง Half Past Twelve"), + ("๐Ÿ•", "๐Ÿ• One O'Clock"), + ("๐Ÿ•œ", "๐Ÿ•œ Half Past One"), + ("๐Ÿ•‘", "๐Ÿ•‘ Two O'Clock"), + ("๐Ÿ•", "๐Ÿ• Half Past Two"), + ("๐Ÿ•’", "๐Ÿ•’ Three O'Clock"), + ("๐Ÿ•ž", "๐Ÿ•ž Half Past Three"), + ("๐Ÿ•“", "๐Ÿ•“ Four O'Clock"), + ("๐Ÿ•Ÿ", "๐Ÿ•Ÿ Half Past Four"), + ("๐Ÿ•”", "๐Ÿ•” Five O'Clock"), + ("๐Ÿ• ", "๐Ÿ•  Half Past Five"), + ("๐Ÿ••", "๐Ÿ•• Six O'Clock"), + ("๐Ÿ•ก", "๐Ÿ•ก Half Past Six"), + ("๐Ÿ•–", "๐Ÿ•– Seven O'Clock"), + ("๐Ÿ•ข", "๐Ÿ•ข Half Past Seven"), + ("๐Ÿ•—", "๐Ÿ•— Eight O'Clock"), + ("๐Ÿ•ฃ", "๐Ÿ•ฃ Half Past Eight"), + ("๐Ÿ•˜", "๐Ÿ•˜ Nine O'Clock"), + ("๐Ÿ•ค", "๐Ÿ•ค Half Past Nine"), + ("๐Ÿ•™", "๐Ÿ•™ Ten O'Clock"), + ("๐Ÿ•ฅ", "๐Ÿ•ฅ Half Past Ten"), + ("๐Ÿ•š", "๐Ÿ•š Eleven O'Clock"), + ("๐Ÿ•ฆ", "๐Ÿ•ฆ Half Past Eleven"), + ("โ›Ž", "โ›Ž Ophiuchus"), + ("๐ŸŒ‘", "๐ŸŒ‘ New Moon"), + ("๐ŸŒ’", "๐ŸŒ’ Waxing Crescent"), + ("๐ŸŒ“", "๐ŸŒ“ First Quarter Moon Symbol"), + ("๐ŸŒ”", "๐ŸŒ” Waxing Gibbous"), + ("๐ŸŒ•", "๐ŸŒ• Full Moon"), + ("๐ŸŒ–", "๐ŸŒ– Waning Gibbous"), + ("๐ŸŒ—", "๐ŸŒ— Half Moon"), + ("๐ŸŒ˜", "๐ŸŒ˜ Waning Crescent"), + ("๐ŸŒ™", "๐ŸŒ™ Crescent Moon"), + ("๐ŸŒš", "๐ŸŒš New Moon With Face"), + ("๐ŸŒ›", "๐ŸŒ› First Quarter Moon With Face"), + ("๐ŸŒœ", "๐ŸŒœ Last Quarter Moon With Face"), + ("๐ŸŒ", "๐ŸŒ Full Moon With Face"), + ("๐ŸŒž", "๐ŸŒž Sun"), + ("๐ŸŒ€", "๐ŸŒ€ Cyclone"), + ("๐ŸŒˆ", "๐ŸŒˆ Rainbow"), + ("๐ŸŒ‚", "๐ŸŒ‚ Umbrella"), + ("๐ŸŒŸ", "๐ŸŒŸ Glowing Star"), + ("๐ŸŒ ", "๐ŸŒ  Shooting Star"), + ("๐Ÿ”ฅ", "๐Ÿ”ฅ Fire"), + ("๐ŸŽƒ", "๐ŸŽƒ Jack-O-Lantern"), + ("๐ŸŽ„", "๐ŸŽ„ Presents Tree"), + ("๐ŸŽ†", "๐ŸŽ† Fireworks"), + ("๐ŸŽ‡", "๐ŸŽ‡ Firework Sparkler"), + ("โœจ", "โœจ Sparkles"), + ("๐ŸŽˆ", "๐ŸŽˆ Balloon"), + ("๐ŸŽ‰", "๐ŸŽ‰ Party Pop"), + ("๐ŸŽŠ", "๐ŸŽŠ Confetti Ball"), + ("๐ŸŽ‹", "๐ŸŽ‹ Tanabata Tree"), + ("๐ŸŽŒ", "๐ŸŽŒ Crossed Flags"), + ("๐ŸŽ", "๐ŸŽ Pine Decoration"), + ("๐ŸŽŽ", "๐ŸŽŽ Japanese Dolls"), + ("๐ŸŽ", "๐ŸŽ Carp Streamer"), + ("๐ŸŽ", "๐ŸŽ Wind Chime"), + ("๐ŸŽ‘", "๐ŸŽ‘ Moon Viewing Ceremony"), + ("๐ŸŽ“", "๐ŸŽ“ Grad Cap"), + ("๐ŸŽฏ", "๐ŸŽฏ Bullseye"), + ("๐ŸŽด", "๐ŸŽด Flower Playing Cards"), + ("๐ŸŽ€", "๐ŸŽ€ Ribbon"), + ("๐ŸŽ", "๐ŸŽ Wrapped Present"), + ("๐ŸŽซ", "๐ŸŽซ Ticket"), + ("๐Ÿ€", "๐Ÿ€ Basketball"), + ("๐Ÿˆ", "๐Ÿˆ America Ball"), + ("๐Ÿ‰", "๐Ÿ‰ Rugby Ball"), + ("๐ŸŽพ", "๐ŸŽพ Tennis"), + ("๐ŸŽฑ", "๐ŸŽฑ Billiards"), + ("๐ŸŽณ", "๐ŸŽณ Bowling"), + ("๐ŸŽฃ", "๐ŸŽฃ Fishing Pole and Fish"), + ("๐ŸŽฝ", "๐ŸŽฝ Running Shirt With Sash"), + ("๐ŸŽฟ", "๐ŸŽฟ Ski and Ski Boot"), + ("๐Ÿ‚", "๐Ÿ‚ Snowboarder"), + ("๐Ÿ„", "๐Ÿ„ Surfer"), + ("๐Ÿ‡", "๐Ÿ‡ Horse Racing"), + ("๐ŸŠ", "๐ŸŠ Swimmer"), + ("๐Ÿšด", "๐Ÿšด Bicyclist"), + ("๐Ÿšต", "๐Ÿšต Mountain Bicyclist"), + ("๐Ÿ†", "๐Ÿ† Trophy"), + ("๐ŸŽฎ", "๐ŸŽฎ Video Game"), + ("๐ŸŽฒ", "๐ŸŽฒ Random Cube"), + ("๐Ÿƒ", "๐Ÿƒ Playing Card Black Joker"), + ("๐Ÿ”‡", "๐Ÿ”‡ Speaker With Cancellation Stroke"), + ("๐Ÿ”ˆ", "๐Ÿ”ˆ Speaker"), + ("๐Ÿ”‰", "๐Ÿ”‰ Speaker With One Sound Wave"), + ("๐Ÿ”Š", "๐Ÿ”Š Speaker With Three Sound Waves"), + ("๐Ÿ“ข", "๐Ÿ“ข Public Address Loudspeaker"), + ("๐Ÿ“ฃ", "๐Ÿ“ฃ Loud Phone"), + ("๐Ÿ“ฏ", "๐Ÿ“ฏ Horn"), + ("๐Ÿ””", "๐Ÿ”” Bell"), + ("๐Ÿ”•", "๐Ÿ”• No Bells"), + ("๐Ÿ”€", "๐Ÿ”€ Shuffle"), + ("๐Ÿ”", "๐Ÿ” Repeat"), + ("๐Ÿ”‚", "๐Ÿ”‚ Repeat Once"), + ("โฉ", "โฉ Fast Forward"), + ("โญ", "โญ Next Track"), + ("โฏ", "โฏ Play/Pause"), + ("โช", "โช Rewind"), + ("โฎ", "โฎ Previous Track"), + ("๐Ÿ”ผ", "๐Ÿ”ผ Up-Pointing Small Red Triangle"), + ("โซ", "โซ Up to Top"), + ("๐Ÿ”ฝ", "๐Ÿ”ฝ Down-Pointing Small Red Triangle"), + ("โฌ", "โฌ Down to Bottom"), + ("๐ŸŽผ", "๐ŸŽผ Musical Score"), + ("๐ŸŽต", "๐ŸŽต Musical Note"), + ("๐ŸŽถ", "๐ŸŽถ Music Notes"), + ("๐ŸŽค", "๐ŸŽค Microphone"), + ("๐ŸŽง", "๐ŸŽง Headphone"), + ("๐ŸŽท", "๐ŸŽท Saxophone"), + ("๐ŸŽธ", "๐ŸŽธ Guitar"), + ("๐ŸŽน", "๐ŸŽน Keyboard"), + ("๐ŸŽบ", "๐ŸŽบ Trumpet"), + ("๐ŸŽป", "๐ŸŽป Violin"), + ("๐Ÿ“ป", "๐Ÿ“ป Boom Box"), + ("๐Ÿ“ฑ", "๐Ÿ“ฑ Internet Phone"), + ("๐Ÿ“ณ", "๐Ÿ“ณ Vibration Mode"), + ("๐Ÿ“ด", "๐Ÿ“ด Mobile Phone Off"), + ("๐Ÿ“ฒ", "๐Ÿ“ฒ Download to Phone"), + ("๐Ÿ“ต", "๐Ÿ“ต No Mobile Phones"), + ("๐Ÿ“ž", "๐Ÿ“ž Old Phone"), + ("๐Ÿ”Ÿ", "๐Ÿ”Ÿ Keycap Ten"), + ("๐Ÿ“ถ", "๐Ÿ“ถ Antenna With Bars"), + ("๐Ÿ“Ÿ", "๐Ÿ“Ÿ Pager"), + ("๐Ÿ“ ", "๐Ÿ“  Fax Machine"), + ("๐Ÿ”‹", "๐Ÿ”‹ Battery"), + ("๐Ÿ”Œ", "๐Ÿ”Œ Plug"), + ("๐Ÿ’ป", "๐Ÿ’ป Personal Computer"), + ("๐Ÿ’ฝ", "๐Ÿ’ฝ Minidisc"), + ("๐Ÿ’พ", "๐Ÿ’พ Floppy"), + ("๐Ÿ’ฟ", "๐Ÿ’ฟ Compact Disc"), + ("๐Ÿ“€", "๐Ÿ“€ DVD"), + ("๐ŸŽฅ", "๐ŸŽฅ Movie Camera"), + ("๐ŸŽฆ", "๐ŸŽฆ Cinema"), + ("๐ŸŽฌ", "๐ŸŽฌ Clapper"), + ("๐Ÿ“บ", "๐Ÿ“บ Television"), + ("๐Ÿ“ท", "๐Ÿ“ท Camera"), + ("๐Ÿ“น", "๐Ÿ“น Video Camera"), + ("๐Ÿ“ผ", "๐Ÿ“ผ Videocassette"), + ("๐Ÿ”…", "๐Ÿ”… Low Brightness Symbol"), + ("๐Ÿ”†", "๐Ÿ”† High Brightness Symbol"), + ("๐Ÿ”", "๐Ÿ” Bigger Glass"), + ("๐Ÿ”Ž", "๐Ÿ”Ž Right-Pointing Magnifying Glass"), + ("๐Ÿ”ฌ", "๐Ÿ”ฌ Microscope"), + ("๐Ÿ”ญ", "๐Ÿ”ญ Telescope"), + ("๐Ÿ“ก", "๐Ÿ“ก Satellite Dish"), + ("๐Ÿ’ก", "๐Ÿ’ก Light Bulb"), + ("๐Ÿ”ฆ", "๐Ÿ”ฆ Electric Torch"), + ("๐Ÿฎ", "๐Ÿฎ Izakaya Lantern"), + ("๐Ÿ“”", "๐Ÿ“” Notebook With Decorative Cover"), + ("๐Ÿ“•", "๐Ÿ“• Closed Book"), + ("๐Ÿ“–", "๐Ÿ“– Open Book"), + ("๐Ÿ“—", "๐Ÿ“— Green Book"), + ("๐Ÿ“˜", "๐Ÿ“˜ Blue Book"), + ("๐Ÿ“™", "๐Ÿ“™ Orange Book"), + ("๐Ÿ“š", "๐Ÿ“š Books"), + ("๐Ÿ““", "๐Ÿ““ Notebook"), + ("๐Ÿ“’", "๐Ÿ“’ Ledger"), + ("๐Ÿ“ƒ", "๐Ÿ“ƒ Page With Curl"), + ("๐Ÿ“œ", "๐Ÿ“œ Scroll"), + ("๐Ÿ“„", "๐Ÿ“„ Page Facing Up"), + ("๐Ÿ“ฐ", "๐Ÿ“ฐ Newspaper"), + ("๐Ÿ“‘", "๐Ÿ“‘ Bookmark Tabs"), + ("๐Ÿ”–", "๐Ÿ”– Bookmark"), + ("๐Ÿ’ฐ", "๐Ÿ’ฐ Money Bag"), + ("๐Ÿ’ด", "๐Ÿ’ด Banknote With Yen Sign"), + ("๐Ÿ’ต", "๐Ÿ’ต Banknote With Dollar Sign"), + ("๐Ÿ’ถ", "๐Ÿ’ถ Banknote With Euro Sign"), + ("๐Ÿ’ท", "๐Ÿ’ท Banknote With Pound Sign"), + ("๐Ÿ’ธ", "๐Ÿ’ธ Flying Money"), + ("๐Ÿ’ฑ", "๐Ÿ’ฑ Currency Exchange"), + ("๐Ÿ’ฒ", "๐Ÿ’ฒ Heavy Dollar Sign"), + ("๐Ÿ’ณ", "๐Ÿ’ณ Credit Card"), + ("๐Ÿ’น", "๐Ÿ’น Upwards Trend in Yen"), + ("๐Ÿ“ง", "๐Ÿ“ง E-Mail Symbol"), + ("๐Ÿ“จ", "๐Ÿ“จ Incoming Envelope"), + ("๐Ÿ“ฉ", "๐Ÿ“ฉ Going Into Envelope"), + ("๐Ÿ“ค", "๐Ÿ“ค Outbox Tray"), + ("๐Ÿ“ฅ", "๐Ÿ“ฅ Inbox Tray"), + ("๐Ÿ“ฆ", "๐Ÿ“ฆ Package"), + ("๐Ÿ“ซ", "๐Ÿ“ซ Mailbox"), + ("๐Ÿ“ช", "๐Ÿ“ช Closed Mailbox With Lowered Flag"), + ("๐Ÿ“ฌ", "๐Ÿ“ฌ Open Mailbox With Raised Flag"), + ("๐Ÿ“ญ", "๐Ÿ“ญ Open Mailbox With Lowered Flag"), + ("๐Ÿ“ฎ", "๐Ÿ“ฎ Postbox"), + ("๐Ÿ“", "๐Ÿ“ Memo"), + ("๐Ÿ’ผ", "๐Ÿ’ผ Briefcase"), + ("๐Ÿ“", "๐Ÿ“ File Folder"), + ("๐Ÿ“‚", "๐Ÿ“‚ Open File Folder"), + ("๐Ÿ“…", "๐Ÿ“… Dated"), + ("๐Ÿ“†", "๐Ÿ“† Tear-Off Calendar"), + ("๐Ÿ“‡", "๐Ÿ“‡ Card Index"), + ("๐Ÿ“ˆ", "๐Ÿ“ˆ Up Trend"), + ("๐Ÿ“‰", "๐Ÿ“‰ Down Trend"), + ("๐Ÿ“Š", "๐Ÿ“Š Bar Chart"), + ("๐Ÿ“‹", "๐Ÿ“‹ Clipboard"), + ("๐Ÿ“Œ", "๐Ÿ“Œ Pushpin"), + ("๐Ÿ“", "๐Ÿ“ Location"), + ("๐Ÿ“Ž", "๐Ÿ“Ž Paperclip"), + ("๐Ÿ“", "๐Ÿ“ Straight Line"), + ("๐Ÿ“", "๐Ÿ“ Three Sides"), + ("๐Ÿ“›", "๐Ÿ“› Name Badge"), + ("๐Ÿ”’", "๐Ÿ”’ Lock"), + ("๐Ÿ”“", "๐Ÿ”“ Open Lock"), + ("๐Ÿ”", "๐Ÿ” Lock With Ink Pen"), + ("๐Ÿ”", "๐Ÿ” Closed Lock With Key"), + ("๐Ÿ”‘", "๐Ÿ”‘ Key"), + ("๐Ÿ”จ", "๐Ÿ”จ Hammer"), + ("๐Ÿ”ง", "๐Ÿ”ง Spanner"), + ("๐Ÿ”ฉ", "๐Ÿ”ฉ Calipers"), + ("๐Ÿ”—", "๐Ÿ”— Link Symbol"), + ("๐Ÿ’‰", "๐Ÿ’‰ Syringe"), + ("๐Ÿ’Š", "๐Ÿ’Š Pill"), + ("๐Ÿ”ช", "๐Ÿ”ช Chef Knife"), + ("๐Ÿ”ซ", "๐Ÿ”ซ Pistol"), + ("๐Ÿšฌ", "๐Ÿšฌ Durry"), + ("๐Ÿ", "๐Ÿ Get Set Go"), + ("๐Ÿšฉ", "๐Ÿšฉ Triangular Flag on Post"), + ("๐Ÿ‡ฆ๐Ÿ‡ซ", "๐Ÿ‡ฆ๐Ÿ‡ซ Afghanistan"), + ("๐Ÿ‡ฆ๐Ÿ‡ฝ", "๐Ÿ‡ฆ๐Ÿ‡ฝ ร…land Islands"), + ("๐Ÿ‡ฆ๐Ÿ‡ฑ", "๐Ÿ‡ฆ๐Ÿ‡ฑ Albania"), + ("๐Ÿ‡ฉ๐Ÿ‡ฟ", "๐Ÿ‡ฉ๐Ÿ‡ฟ Algeria"), + ("๐Ÿ‡ฆ๐Ÿ‡ธ", "๐Ÿ‡ฆ๐Ÿ‡ธ American Samoa"), + ("๐Ÿ‡ฆ๐Ÿ‡ฉ", "๐Ÿ‡ฆ๐Ÿ‡ฉ Andorra"), + ("๐Ÿ‡ฆ๐Ÿ‡ด", "๐Ÿ‡ฆ๐Ÿ‡ด Angola"), + ("๐Ÿ‡ฆ๐Ÿ‡ฎ", "๐Ÿ‡ฆ๐Ÿ‡ฎ Anguilla"), + ("๐Ÿ‡ฆ๐Ÿ‡ถ", "๐Ÿ‡ฆ๐Ÿ‡ถ Antarctica"), + ("๐Ÿ‡ฆ๐Ÿ‡ฌ", "๐Ÿ‡ฆ๐Ÿ‡ฌ Antigua & Barbuda"), + ("๐Ÿ‡ฆ๐Ÿ‡ท", "๐Ÿ‡ฆ๐Ÿ‡ท Argentina"), + ("๐Ÿ‡ฆ๐Ÿ‡ฒ", "๐Ÿ‡ฆ๐Ÿ‡ฒ Armenia"), + ("๐Ÿ‡ฆ๐Ÿ‡ผ", "๐Ÿ‡ฆ๐Ÿ‡ผ Aruba"), + ("๐Ÿ‡ฆ๐Ÿ‡จ", "๐Ÿ‡ฆ๐Ÿ‡จ Ascension Island"), + ("๐Ÿ‡ฆ๐Ÿ‡บ", "๐Ÿ‡ฆ๐Ÿ‡บ Australia"), + ("๐Ÿ‡ฆ๐Ÿ‡น", "๐Ÿ‡ฆ๐Ÿ‡น Austria"), + ("๐Ÿ‡ฆ๐Ÿ‡ฟ", "๐Ÿ‡ฆ๐Ÿ‡ฟ Azerbaijan"), + ("๐Ÿ‡ง๐Ÿ‡ธ", "๐Ÿ‡ง๐Ÿ‡ธ Bahamas"), + ("๐Ÿ‡ง๐Ÿ‡ญ", "๐Ÿ‡ง๐Ÿ‡ญ Bahrain"), + ("๐Ÿ‡ง๐Ÿ‡ฉ", "๐Ÿ‡ง๐Ÿ‡ฉ Bangladesh"), + ("๐Ÿ‡ง๐Ÿ‡ง", "๐Ÿ‡ง๐Ÿ‡ง Barbados"), + ("๐Ÿ‡ง๐Ÿ‡พ", "๐Ÿ‡ง๐Ÿ‡พ Belarus"), + ("๐Ÿ‡ง๐Ÿ‡ช", "๐Ÿ‡ง๐Ÿ‡ช Belgium"), + ("๐Ÿ‡ง๐Ÿ‡ฟ", "๐Ÿ‡ง๐Ÿ‡ฟ Belize"), + ("๐Ÿ‡ง๐Ÿ‡ฏ", "๐Ÿ‡ง๐Ÿ‡ฏ Benin"), + ("๐Ÿ‡ง๐Ÿ‡ฒ", "๐Ÿ‡ง๐Ÿ‡ฒ Bermuda"), + ("๐Ÿ‡ง๐Ÿ‡น", "๐Ÿ‡ง๐Ÿ‡น Bhutan"), + ("๐Ÿ‡ง๐Ÿ‡ด", "๐Ÿ‡ง๐Ÿ‡ด Bolivia"), + ("๐Ÿ‡ง๐Ÿ‡ฆ", "๐Ÿ‡ง๐Ÿ‡ฆ Bosnia & Herzegovina"), + ("๐Ÿ‡ง๐Ÿ‡ผ", "๐Ÿ‡ง๐Ÿ‡ผ Botswana"), + ("๐Ÿ‡ง๐Ÿ‡ป", "๐Ÿ‡ง๐Ÿ‡ป Bouvet Island"), + ("๐Ÿ‡ง๐Ÿ‡ท", "๐Ÿ‡ง๐Ÿ‡ท Brazil"), + ("๐Ÿ‡ฎ๐Ÿ‡ด", "๐Ÿ‡ฎ๐Ÿ‡ด British Indian Ocean Territory"), + ("๐Ÿ‡ป๐Ÿ‡ฌ", "๐Ÿ‡ป๐Ÿ‡ฌ British Virgin Islands"), + ("๐Ÿ‡ง๐Ÿ‡ณ", "๐Ÿ‡ง๐Ÿ‡ณ Brunei"), + ("๐Ÿ‡ง๐Ÿ‡ฌ", "๐Ÿ‡ง๐Ÿ‡ฌ Bulgaria"), + ("๐Ÿ‡ง๐Ÿ‡ซ", "๐Ÿ‡ง๐Ÿ‡ซ Burkina Faso"), + ("๐Ÿ‡ง๐Ÿ‡ฎ", "๐Ÿ‡ง๐Ÿ‡ฎ Burundi"), + ("๐Ÿ‡ฐ๐Ÿ‡ญ", "๐Ÿ‡ฐ๐Ÿ‡ญ Cambodia"), + ("๐Ÿ‡จ๐Ÿ‡ฒ", "๐Ÿ‡จ๐Ÿ‡ฒ Cameroon"), + ("๐Ÿ‡จ๐Ÿ‡ฆ", "๐Ÿ‡จ๐Ÿ‡ฆ Canada"), + ("๐Ÿ‡ฎ๐Ÿ‡จ", "๐Ÿ‡ฎ๐Ÿ‡จ Canary Islands"), + ("๐Ÿ‡จ๐Ÿ‡ป", "๐Ÿ‡จ๐Ÿ‡ป Cape Verde"), + ("๐Ÿ‡ง๐Ÿ‡ถ", "๐Ÿ‡ง๐Ÿ‡ถ Caribbean Netherlands"), + ("๐Ÿ‡ฐ๐Ÿ‡พ", "๐Ÿ‡ฐ๐Ÿ‡พ Cayman Islands"), + ("๐Ÿ‡จ๐Ÿ‡ซ", "๐Ÿ‡จ๐Ÿ‡ซ Central African Republic"), + ("๐Ÿ‡ช๐Ÿ‡ฆ", "๐Ÿ‡ช๐Ÿ‡ฆ Ceuta & Melilla"), + ("๐Ÿ‡น๐Ÿ‡ฉ", "๐Ÿ‡น๐Ÿ‡ฉ Chad"), + ("๐Ÿ‡จ๐Ÿ‡ฑ", "๐Ÿ‡จ๐Ÿ‡ฑ Chile"), + ("๐Ÿ‡จ๐Ÿ‡ณ", "๐Ÿ‡จ๐Ÿ‡ณ China"), + ("๐Ÿ‡จ๐Ÿ‡ฝ", "๐Ÿ‡จ๐Ÿ‡ฝ Christmas Island"), + ("๐Ÿ‡จ๐Ÿ‡ต", "๐Ÿ‡จ๐Ÿ‡ต Clipperton Island"), + ("๐Ÿ‡จ๐Ÿ‡จ", "๐Ÿ‡จ๐Ÿ‡จ Cocos Islands"), + ("๐Ÿ‡จ๐Ÿ‡ด", "๐Ÿ‡จ๐Ÿ‡ด Colombia"), + ("๐Ÿ‡ฐ๐Ÿ‡ฒ", "๐Ÿ‡ฐ๐Ÿ‡ฒ Comoros"), + ("๐Ÿ‡จ๐Ÿ‡ฌ", "๐Ÿ‡จ๐Ÿ‡ฌ Congo - Brazzaville"), + ("๐Ÿ‡จ๐Ÿ‡ฉ", "๐Ÿ‡จ๐Ÿ‡ฉ Congo - Kinshasa"), + ("๐Ÿ‡จ๐Ÿ‡ฐ", "๐Ÿ‡จ๐Ÿ‡ฐ Cook Islands"), + ("๐Ÿ‡จ๐Ÿ‡ท", "๐Ÿ‡จ๐Ÿ‡ท Costa Rica"), + ("๐Ÿ‡จ๐Ÿ‡ฎ", "๐Ÿ‡จ๐Ÿ‡ฎ Cรดte Dโ€™Ivoire"), + ("๐Ÿ‡ญ๐Ÿ‡ท", "๐Ÿ‡ญ๐Ÿ‡ท Croatia"), + ("๐Ÿ‡จ๐Ÿ‡บ", "๐Ÿ‡จ๐Ÿ‡บ Cuba"), + ("๐Ÿ‡จ๐Ÿ‡ผ", "๐Ÿ‡จ๐Ÿ‡ผ Curaรงao"), + ("๐Ÿ‡จ๐Ÿ‡พ", "๐Ÿ‡จ๐Ÿ‡พ Cyprus"), + ("๐Ÿ‡จ๐Ÿ‡ฟ", "๐Ÿ‡จ๐Ÿ‡ฟ Czech Republic"), + ("๐Ÿ‡ฉ๐Ÿ‡ฐ", "๐Ÿ‡ฉ๐Ÿ‡ฐ Denmark"), + ("๐Ÿ‡ฉ๐Ÿ‡ฌ", "๐Ÿ‡ฉ๐Ÿ‡ฌ Diego Garcia"), + ("๐Ÿ‡ฉ๐Ÿ‡ฏ", "๐Ÿ‡ฉ๐Ÿ‡ฏ Djibouti"), + ("๐Ÿ‡ฉ๐Ÿ‡ฒ", "๐Ÿ‡ฉ๐Ÿ‡ฒ Dominica"), + ("๐Ÿ‡ฉ๐Ÿ‡ด", "๐Ÿ‡ฉ๐Ÿ‡ด Dominican Republic"), + ("๐Ÿ‡ช๐Ÿ‡จ", "๐Ÿ‡ช๐Ÿ‡จ Ecuador"), + ("๐Ÿ‡ช๐Ÿ‡ฌ", "๐Ÿ‡ช๐Ÿ‡ฌ Egypt"), + ("๐Ÿ‡ธ๐Ÿ‡ป", "๐Ÿ‡ธ๐Ÿ‡ป El Salvador"), + ("๐Ÿ‡ฌ๐Ÿ‡ถ", "๐Ÿ‡ฌ๐Ÿ‡ถ Equatorial Guinea"), + ("๐Ÿ‡ช๐Ÿ‡ท", "๐Ÿ‡ช๐Ÿ‡ท Eritrea"), + ("๐Ÿ‡ช๐Ÿ‡ช", "๐Ÿ‡ช๐Ÿ‡ช Estonia"), + ("๐Ÿ‡ช๐Ÿ‡น", "๐Ÿ‡ช๐Ÿ‡น Ethiopia"), + ("๐Ÿ‡ช๐Ÿ‡บ", "๐Ÿ‡ช๐Ÿ‡บ European Union"), + ("๐Ÿ‡ซ๐Ÿ‡ฐ", "๐Ÿ‡ซ๐Ÿ‡ฐ Falkland Islands"), + ("๐Ÿ‡ซ๐Ÿ‡ด", "๐Ÿ‡ซ๐Ÿ‡ด Faroe Islands"), + ("๐Ÿ‡ซ๐Ÿ‡ฏ", "๐Ÿ‡ซ๐Ÿ‡ฏ Fiji"), + ("๐Ÿ‡ซ๐Ÿ‡ฎ", "๐Ÿ‡ซ๐Ÿ‡ฎ Finland"), + ("๐Ÿ‡ซ๐Ÿ‡ท", "๐Ÿ‡ซ๐Ÿ‡ท France"), + ("๐Ÿ‡ฌ๐Ÿ‡ซ", "๐Ÿ‡ฌ๐Ÿ‡ซ French Guiana"), + ("๐Ÿ‡ต๐Ÿ‡ซ", "๐Ÿ‡ต๐Ÿ‡ซ French Polynesia"), + ("๐Ÿ‡น๐Ÿ‡ซ", "๐Ÿ‡น๐Ÿ‡ซ French Southern Territories"), + ("๐Ÿ‡ฌ๐Ÿ‡ฆ", "๐Ÿ‡ฌ๐Ÿ‡ฆ Gabon"), + ("๐Ÿ‡ฌ๐Ÿ‡ฒ", "๐Ÿ‡ฌ๐Ÿ‡ฒ Gambia"), + ("๐Ÿ‡ฌ๐Ÿ‡ช", "๐Ÿ‡ฌ๐Ÿ‡ช Georgia"), + ("๐Ÿ‡ฉ๐Ÿ‡ช", "๐Ÿ‡ฉ๐Ÿ‡ช Germany"), + ("๐Ÿ‡ฌ๐Ÿ‡ญ", "๐Ÿ‡ฌ๐Ÿ‡ญ Ghana"), + ("๐Ÿ‡ฌ๐Ÿ‡ฎ", "๐Ÿ‡ฌ๐Ÿ‡ฎ Gibraltar"), + ("๐Ÿ‡ฌ๐Ÿ‡ท", "๐Ÿ‡ฌ๐Ÿ‡ท Greece"), + ("๐Ÿ‡ฌ๐Ÿ‡ฑ", "๐Ÿ‡ฌ๐Ÿ‡ฑ Greenland"), + ("๐Ÿ‡ฌ๐Ÿ‡ฉ", "๐Ÿ‡ฌ๐Ÿ‡ฉ Grenada"), + ("๐Ÿ‡ฌ๐Ÿ‡ต", "๐Ÿ‡ฌ๐Ÿ‡ต Guadeloupe"), + ("๐Ÿ‡ฌ๐Ÿ‡บ", "๐Ÿ‡ฌ๐Ÿ‡บ Guam"), + ("๐Ÿ‡ฌ๐Ÿ‡น", "๐Ÿ‡ฌ๐Ÿ‡น Guatemala"), + ("๐Ÿ‡ฌ๐Ÿ‡ฌ", "๐Ÿ‡ฌ๐Ÿ‡ฌ Guernsey"), + ("๐Ÿ‡ฌ๐Ÿ‡ณ", "๐Ÿ‡ฌ๐Ÿ‡ณ Guinea"), + ("๐Ÿ‡ฌ๐Ÿ‡ผ", "๐Ÿ‡ฌ๐Ÿ‡ผ Guinea-Bissau"), + ("๐Ÿ‡ฌ๐Ÿ‡พ", "๐Ÿ‡ฌ๐Ÿ‡พ Guyana"), + ("๐Ÿ‡ญ๐Ÿ‡น", "๐Ÿ‡ญ๐Ÿ‡น Haiti"), + ("๐Ÿ‡ญ๐Ÿ‡ฒ", "๐Ÿ‡ญ๐Ÿ‡ฒ Heard & McDonald Islands"), + ("๐Ÿ‡ญ๐Ÿ‡ณ", "๐Ÿ‡ญ๐Ÿ‡ณ Honduras"), + ("๐Ÿ‡ญ๐Ÿ‡ฐ", "๐Ÿ‡ญ๐Ÿ‡ฐ Hong Kong"), + ("๐Ÿ‡ญ๐Ÿ‡บ", "๐Ÿ‡ญ๐Ÿ‡บ Hungary"), + ("๐Ÿ‡ฎ๐Ÿ‡ธ", "๐Ÿ‡ฎ๐Ÿ‡ธ Iceland"), + ("๐Ÿ‡ฎ๐Ÿ‡ณ", "๐Ÿ‡ฎ๐Ÿ‡ณ India"), + ("๐Ÿ‡ฎ๐Ÿ‡ฉ", "๐Ÿ‡ฎ๐Ÿ‡ฉ Indonesia"), + ("๐Ÿ‡ฎ๐Ÿ‡ท", "๐Ÿ‡ฎ๐Ÿ‡ท Iran"), + ("๐Ÿ‡ฎ๐Ÿ‡ถ", "๐Ÿ‡ฎ๐Ÿ‡ถ Iraq"), + ("๐Ÿ‡ฎ๐Ÿ‡ช", "๐Ÿ‡ฎ๐Ÿ‡ช Ireland"), + ("๐Ÿ‡ฎ๐Ÿ‡ฒ", "๐Ÿ‡ฎ๐Ÿ‡ฒ Isle of Man"), + ("๐Ÿ‡ฎ๐Ÿ‡ฑ", "๐Ÿ‡ฎ๐Ÿ‡ฑ Israel"), + ("๐Ÿ‡ฎ๐Ÿ‡น", "๐Ÿ‡ฎ๐Ÿ‡น Italy"), + ("๐Ÿ‡ฏ๐Ÿ‡ฒ", "๐Ÿ‡ฏ๐Ÿ‡ฒ Jamaica"), + ("๐Ÿ‡ฏ๐Ÿ‡ต", "๐Ÿ‡ฏ๐Ÿ‡ต Japan"), + ("๐Ÿ‡ฏ๐Ÿ‡ช", "๐Ÿ‡ฏ๐Ÿ‡ช Jersey"), + ("๐Ÿ‡ฏ๐Ÿ‡ด", "๐Ÿ‡ฏ๐Ÿ‡ด Jordan"), + ("๐Ÿ‡ฐ๐Ÿ‡ฟ", "๐Ÿ‡ฐ๐Ÿ‡ฟ Kazakhstan"), + ("๐Ÿ‡ฐ๐Ÿ‡ช", "๐Ÿ‡ฐ๐Ÿ‡ช Kenya"), + ("๐Ÿ‡ฐ๐Ÿ‡ฎ", "๐Ÿ‡ฐ๐Ÿ‡ฎ Kiribati"), + ("๐Ÿ‡ฝ๐Ÿ‡ฐ", "๐Ÿ‡ฝ๐Ÿ‡ฐ Kosovo"), + ("๐Ÿ‡ฐ๐Ÿ‡ผ", "๐Ÿ‡ฐ๐Ÿ‡ผ Kuwait"), + ("๐Ÿ‡ฐ๐Ÿ‡ฌ", "๐Ÿ‡ฐ๐Ÿ‡ฌ Kyrgyzstan"), + ("๐Ÿ‡ฑ๐Ÿ‡ฆ", "๐Ÿ‡ฑ๐Ÿ‡ฆ Laos"), + ("๐Ÿ‡ฑ๐Ÿ‡ป", "๐Ÿ‡ฑ๐Ÿ‡ป Latvia"), + ("๐Ÿ‡ฑ๐Ÿ‡ง", "๐Ÿ‡ฑ๐Ÿ‡ง Lebanon"), + ("๐Ÿ‡ฑ๐Ÿ‡ธ", "๐Ÿ‡ฑ๐Ÿ‡ธ Lesotho"), + ("๐Ÿ‡ฑ๐Ÿ‡ท", "๐Ÿ‡ฑ๐Ÿ‡ท Liberia"), + ("๐Ÿ‡ฑ๐Ÿ‡พ", "๐Ÿ‡ฑ๐Ÿ‡พ Libya"), + ("๐Ÿ‡ฑ๐Ÿ‡ฎ", "๐Ÿ‡ฑ๐Ÿ‡ฎ Liechtenstein"), + ("๐Ÿ‡ฑ๐Ÿ‡น", "๐Ÿ‡ฑ๐Ÿ‡น Lithuania"), + ("๐Ÿ‡ฑ๐Ÿ‡บ", "๐Ÿ‡ฑ๐Ÿ‡บ Luxembourg"), + ("๐Ÿ‡ฒ๐Ÿ‡ด", "๐Ÿ‡ฒ๐Ÿ‡ด Macau"), + ("๐Ÿ‡ฒ๐Ÿ‡ฐ", "๐Ÿ‡ฒ๐Ÿ‡ฐ Macedonia"), + ("๐Ÿ‡ฒ๐Ÿ‡ฌ", "๐Ÿ‡ฒ๐Ÿ‡ฌ Madagascar"), + ("๐Ÿ‡ฒ๐Ÿ‡ผ", "๐Ÿ‡ฒ๐Ÿ‡ผ Malawi"), + ("๐Ÿ‡ฒ๐Ÿ‡พ", "๐Ÿ‡ฒ๐Ÿ‡พ Malaysia"), + ("๐Ÿ‡ฒ๐Ÿ‡ป", "๐Ÿ‡ฒ๐Ÿ‡ป Maldives"), + ("๐Ÿ‡ฒ๐Ÿ‡ฑ", "๐Ÿ‡ฒ๐Ÿ‡ฑ Mali"), + ("๐Ÿ‡ฒ๐Ÿ‡น", "๐Ÿ‡ฒ๐Ÿ‡น Malta"), + ("๐Ÿ‡ฒ๐Ÿ‡ญ", "๐Ÿ‡ฒ๐Ÿ‡ญ Marshall Islands"), + ("๐Ÿ‡ฒ๐Ÿ‡ถ", "๐Ÿ‡ฒ๐Ÿ‡ถ Martinique"), + ("๐Ÿ‡ฒ๐Ÿ‡ท", "๐Ÿ‡ฒ๐Ÿ‡ท Mauritania"), + ("๐Ÿ‡ฒ๐Ÿ‡บ", "๐Ÿ‡ฒ๐Ÿ‡บ Mauritius"), + ("๐Ÿ‡พ๐Ÿ‡น", "๐Ÿ‡พ๐Ÿ‡น Mayotte"), + ("๐Ÿ‡ฒ๐Ÿ‡ฝ", "๐Ÿ‡ฒ๐Ÿ‡ฝ Mexico"), + ("๐Ÿ‡ซ๐Ÿ‡ฒ", "๐Ÿ‡ซ๐Ÿ‡ฒ Micronesia"), + ("๐Ÿ‡ฒ๐Ÿ‡ฉ", "๐Ÿ‡ฒ๐Ÿ‡ฉ Moldova"), + ("๐Ÿ‡ฒ๐Ÿ‡จ", "๐Ÿ‡ฒ๐Ÿ‡จ Monaco"), + ("๐Ÿ‡ฒ๐Ÿ‡ณ", "๐Ÿ‡ฒ๐Ÿ‡ณ Mongolia"), + ("๐Ÿ‡ฒ๐Ÿ‡ช", "๐Ÿ‡ฒ๐Ÿ‡ช Montenegro"), + ("๐Ÿ‡ฒ๐Ÿ‡ธ", "๐Ÿ‡ฒ๐Ÿ‡ธ Montserrat"), + ("๐Ÿ‡ฒ๐Ÿ‡ฆ", "๐Ÿ‡ฒ๐Ÿ‡ฆ Morocco"), + ("๐Ÿ‡ฒ๐Ÿ‡ฟ", "๐Ÿ‡ฒ๐Ÿ‡ฟ Mozambique"), + ("๐Ÿ‡ฒ๐Ÿ‡ฒ", "๐Ÿ‡ฒ๐Ÿ‡ฒ Myanmar"), + ("๐Ÿ‡ณ๐Ÿ‡ฆ", "๐Ÿ‡ณ๐Ÿ‡ฆ Namibia"), + ("๐Ÿ‡ณ๐Ÿ‡ท", "๐Ÿ‡ณ๐Ÿ‡ท Nauru"), + ("๐Ÿ‡ณ๐Ÿ‡ต", "๐Ÿ‡ณ๐Ÿ‡ต Nepal"), + ("๐Ÿ‡ณ๐Ÿ‡ฑ", "๐Ÿ‡ณ๐Ÿ‡ฑ Netherlands"), + ("๐Ÿ‡ณ๐Ÿ‡จ", "๐Ÿ‡ณ๐Ÿ‡จ New Caledonia"), + ("๐Ÿ‡ณ๐Ÿ‡ฟ", "๐Ÿ‡ณ๐Ÿ‡ฟ New Zealand"), + ("๐Ÿ‡ณ๐Ÿ‡ฎ", "๐Ÿ‡ณ๐Ÿ‡ฎ Nicaragua"), + ("๐Ÿ‡ณ๐Ÿ‡ช", "๐Ÿ‡ณ๐Ÿ‡ช Niger"), + ("๐Ÿ‡ณ๐Ÿ‡ฌ", "๐Ÿ‡ณ๐Ÿ‡ฌ Nigeria"), + ("๐Ÿ‡ณ๐Ÿ‡บ", "๐Ÿ‡ณ๐Ÿ‡บ Niue"), + ("๐Ÿ‡ณ๐Ÿ‡ซ", "๐Ÿ‡ณ๐Ÿ‡ซ Norfolk Island"), + ("๐Ÿ‡ฒ๐Ÿ‡ต", "๐Ÿ‡ฒ๐Ÿ‡ต Northern Mariana Islands"), + ("๐Ÿ‡ฐ๐Ÿ‡ต", "๐Ÿ‡ฐ๐Ÿ‡ต North Korea"), + ("๐Ÿ‡ณ๐Ÿ‡ด", "๐Ÿ‡ณ๐Ÿ‡ด Norway"), + ("๐Ÿ‡ด๐Ÿ‡ฒ", "๐Ÿ‡ด๐Ÿ‡ฒ Oman"), + ("๐Ÿ‡ต๐Ÿ‡ฐ", "๐Ÿ‡ต๐Ÿ‡ฐ Pakistan"), + ("๐Ÿ‡ต๐Ÿ‡ผ", "๐Ÿ‡ต๐Ÿ‡ผ Palau"), + ("๐Ÿ‡ต๐Ÿ‡ธ", "๐Ÿ‡ต๐Ÿ‡ธ Palestinian Territories"), + ("๐Ÿ‡ต๐Ÿ‡ฆ", "๐Ÿ‡ต๐Ÿ‡ฆ Panama"), + ("๐Ÿ‡ต๐Ÿ‡ฌ", "๐Ÿ‡ต๐Ÿ‡ฌ Papua New Guinea"), + ("๐Ÿ‡ต๐Ÿ‡พ", "๐Ÿ‡ต๐Ÿ‡พ Paraguay"), + ("๐Ÿ‡ต๐Ÿ‡ช", "๐Ÿ‡ต๐Ÿ‡ช Peru"), + ("๐Ÿ‡ต๐Ÿ‡ญ", "๐Ÿ‡ต๐Ÿ‡ญ Philippines"), + ("๐Ÿ‡ต๐Ÿ‡ณ", "๐Ÿ‡ต๐Ÿ‡ณ Pitcairn Islands"), + ("๐Ÿ‡ต๐Ÿ‡ฑ", "๐Ÿ‡ต๐Ÿ‡ฑ Poland"), + ("๐Ÿ‡ต๐Ÿ‡น", "๐Ÿ‡ต๐Ÿ‡น Portugal"), + ("๐Ÿ‡ต๐Ÿ‡ท", "๐Ÿ‡ต๐Ÿ‡ท Puerto Rico"), + ("๐Ÿ‡ถ๐Ÿ‡ฆ", "๐Ÿ‡ถ๐Ÿ‡ฆ Qatar"), + ("๐Ÿ‡ท๐Ÿ‡ช", "๐Ÿ‡ท๐Ÿ‡ช Rรฉunion"), + ("๐Ÿ‡ท๐Ÿ‡ด", "๐Ÿ‡ท๐Ÿ‡ด Romania"), + ("๐Ÿ‡ท๐Ÿ‡บ", "๐Ÿ‡ท๐Ÿ‡บ Russia"), + ("๐Ÿ‡ท๐Ÿ‡ผ", "๐Ÿ‡ท๐Ÿ‡ผ Rwanda"), + ("๐Ÿ‡ผ๐Ÿ‡ธ", "๐Ÿ‡ผ๐Ÿ‡ธ Samoa"), + ("๐Ÿ‡ธ๐Ÿ‡ฒ", "๐Ÿ‡ธ๐Ÿ‡ฒ San Marino"), + ("๐Ÿ‡ธ๐Ÿ‡น", "๐Ÿ‡ธ๐Ÿ‡น Sรฃo Tomรฉ & Prรญncipe"), + ("๐Ÿ‡ธ๐Ÿ‡ฆ", "๐Ÿ‡ธ๐Ÿ‡ฆ Saudi Arabia"), + ("๐Ÿ‡ธ๐Ÿ‡ณ", "๐Ÿ‡ธ๐Ÿ‡ณ Senegal"), + ("๐Ÿ‡ท๐Ÿ‡ธ", "๐Ÿ‡ท๐Ÿ‡ธ Serbia"), + ("๐Ÿ‡ธ๐Ÿ‡จ", "๐Ÿ‡ธ๐Ÿ‡จ Seychelles"), + ("๐Ÿ‡ธ๐Ÿ‡ฑ", "๐Ÿ‡ธ๐Ÿ‡ฑ Sierra Leone"), + ("๐Ÿ‡ธ๐Ÿ‡ฌ", "๐Ÿ‡ธ๐Ÿ‡ฌ Singapore"), + ("๐Ÿ‡ธ๐Ÿ‡ฝ", "๐Ÿ‡ธ๐Ÿ‡ฝ Sint Maarten"), + ("๐Ÿ‡ธ๐Ÿ‡ฐ", "๐Ÿ‡ธ๐Ÿ‡ฐ Slovakia"), + ("๐Ÿ‡ธ๐Ÿ‡ฎ", "๐Ÿ‡ธ๐Ÿ‡ฎ Slovenia"), + ("๐Ÿ‡ธ๐Ÿ‡ง", "๐Ÿ‡ธ๐Ÿ‡ง Solomon Islands"), + ("๐Ÿ‡ธ๐Ÿ‡ด", "๐Ÿ‡ธ๐Ÿ‡ด Somalia"), + ("๐Ÿ‡ฟ๐Ÿ‡ฆ", "๐Ÿ‡ฟ๐Ÿ‡ฆ South Africa"), + ("๐Ÿ‡ฌ๐Ÿ‡ธ", "๐Ÿ‡ฌ๐Ÿ‡ธ South Georgia & South Sandwich Islands"), + ("๐Ÿ‡ฐ๐Ÿ‡ท", "๐Ÿ‡ฐ๐Ÿ‡ท South Korea"), + ("๐Ÿ‡ธ๐Ÿ‡ธ", "๐Ÿ‡ธ๐Ÿ‡ธ South Sudan"), + ("๐Ÿ‡ช๐Ÿ‡ธ", "๐Ÿ‡ช๐Ÿ‡ธ Spain"), + ("๐Ÿ‡ฑ๐Ÿ‡ฐ", "๐Ÿ‡ฑ๐Ÿ‡ฐ Sri Lanka"), + ("๐Ÿ‡ง๐Ÿ‡ฑ", "๐Ÿ‡ง๐Ÿ‡ฑ St. Barthรฉlemy"), + ("๐Ÿ‡ธ๐Ÿ‡ญ", "๐Ÿ‡ธ๐Ÿ‡ญ St. Helena"), + ("๐Ÿ‡ฐ๐Ÿ‡ณ", "๐Ÿ‡ฐ๐Ÿ‡ณ St. Kitts & Nevis"), + ("๐Ÿ‡ฑ๐Ÿ‡จ", "๐Ÿ‡ฑ๐Ÿ‡จ St. Lucia"), + ("๐Ÿ‡ฒ๐Ÿ‡ซ", "๐Ÿ‡ฒ๐Ÿ‡ซ St. Martin"), + ("๐Ÿ‡ต๐Ÿ‡ฒ", "๐Ÿ‡ต๐Ÿ‡ฒ St. Pierre & Miquelon"), + ("๐Ÿ‡ป๐Ÿ‡จ", "๐Ÿ‡ป๐Ÿ‡จ St. Vincent & Grenadines"), + ("๐Ÿ‡ธ๐Ÿ‡ฉ", "๐Ÿ‡ธ๐Ÿ‡ฉ Sudan"), + ("๐Ÿ‡ธ๐Ÿ‡ท", "๐Ÿ‡ธ๐Ÿ‡ท Suriname"), + ("๐Ÿ‡ธ๐Ÿ‡ฏ", "๐Ÿ‡ธ๐Ÿ‡ฏ Svalbard & Jan Mayen"), + ("๐Ÿ‡ธ๐Ÿ‡ฟ", "๐Ÿ‡ธ๐Ÿ‡ฟ Swaziland"), + ("๐Ÿ‡ธ๐Ÿ‡ช", "๐Ÿ‡ธ๐Ÿ‡ช Sweden"), + ("๐Ÿ‡จ๐Ÿ‡ญ", "๐Ÿ‡จ๐Ÿ‡ญ Switzerland"), + ("๐Ÿ‡ธ๐Ÿ‡พ", "๐Ÿ‡ธ๐Ÿ‡พ Syria"), + ("๐Ÿ‡น๐Ÿ‡ผ", "๐Ÿ‡น๐Ÿ‡ผ Taiwan"), + ("๐Ÿ‡น๐Ÿ‡ฏ", "๐Ÿ‡น๐Ÿ‡ฏ Tajikistan"), + ("๐Ÿ‡น๐Ÿ‡ฟ", "๐Ÿ‡น๐Ÿ‡ฟ Tanzania"), + ("๐Ÿ‡น๐Ÿ‡ญ", "๐Ÿ‡น๐Ÿ‡ญ Thailand"), + ("๐Ÿ‡น๐Ÿ‡ฑ", "๐Ÿ‡น๐Ÿ‡ฑ Timor-Leste"), + ("๐Ÿ‡น๐Ÿ‡ฌ", "๐Ÿ‡น๐Ÿ‡ฌ Togo"), + ("๐Ÿ‡น๐Ÿ‡ฐ", "๐Ÿ‡น๐Ÿ‡ฐ Tokelau"), + ("๐Ÿ‡น๐Ÿ‡ด", "๐Ÿ‡น๐Ÿ‡ด Tonga"), + ("๐Ÿ‡น๐Ÿ‡น", "๐Ÿ‡น๐Ÿ‡น Trinidad & Tobago"), + ("๐Ÿ‡น๐Ÿ‡ฆ", "๐Ÿ‡น๐Ÿ‡ฆ Tristan Da Cunha"), + ("๐Ÿ‡น๐Ÿ‡ณ", "๐Ÿ‡น๐Ÿ‡ณ Tunisia"), + ("๐Ÿ‡น๐Ÿ‡ท", "๐Ÿ‡น๐Ÿ‡ท Turkey"), + ("๐Ÿ‡น๐Ÿ‡ฒ", "๐Ÿ‡น๐Ÿ‡ฒ Turkmenistan"), + ("๐Ÿ‡น๐Ÿ‡จ", "๐Ÿ‡น๐Ÿ‡จ Turks & Caicos Islands"), + ("๐Ÿ‡น๐Ÿ‡ป", "๐Ÿ‡น๐Ÿ‡ป Tuvalu"), + ("๐Ÿ‡บ๐Ÿ‡ฌ", "๐Ÿ‡บ๐Ÿ‡ฌ Uganda"), + ("๐Ÿ‡บ๐Ÿ‡ฆ", "๐Ÿ‡บ๐Ÿ‡ฆ Ukraine"), + ("๐Ÿ‡ฆ๐Ÿ‡ช", "๐Ÿ‡ฆ๐Ÿ‡ช United Arab Emirates"), + ("๐Ÿ‡ฌ๐Ÿ‡ง", "๐Ÿ‡ฌ๐Ÿ‡ง United Kingdom"), + ("๐Ÿ‡บ๐Ÿ‡ธ", "๐Ÿ‡บ๐Ÿ‡ธ United States"), + ("๐Ÿ‡บ๐Ÿ‡พ", "๐Ÿ‡บ๐Ÿ‡พ Uruguay"), + ("๐Ÿ‡บ๐Ÿ‡ฒ", "๐Ÿ‡บ๐Ÿ‡ฒ U.S. Outlying Islands"), + ("๐Ÿ‡ป๐Ÿ‡ฎ", "๐Ÿ‡ป๐Ÿ‡ฎ U.S. Virgin Islands"), + ("๐Ÿ‡บ๐Ÿ‡ฟ", "๐Ÿ‡บ๐Ÿ‡ฟ Uzbekistan"), + ("๐Ÿ‡ป๐Ÿ‡บ", "๐Ÿ‡ป๐Ÿ‡บ Vanuatu"), + ("๐Ÿ‡ป๐Ÿ‡ฆ", "๐Ÿ‡ป๐Ÿ‡ฆ Vatican City"), + ("๐Ÿ‡ป๐Ÿ‡ช", "๐Ÿ‡ป๐Ÿ‡ช Venezuela"), + ("๐Ÿ‡ป๐Ÿ‡ณ", "๐Ÿ‡ป๐Ÿ‡ณ Vietnam"), + ("๐Ÿ‡ผ๐Ÿ‡ซ", "๐Ÿ‡ผ๐Ÿ‡ซ Wallis & Futuna"), + ("๐Ÿ‡ช๐Ÿ‡ญ", "๐Ÿ‡ช๐Ÿ‡ญ Western Sahara"), + ("๐Ÿ‡พ๐Ÿ‡ช", "๐Ÿ‡พ๐Ÿ‡ช Yemen"), + ("๐Ÿ‡ฟ๐Ÿ‡ฒ", "๐Ÿ‡ฟ๐Ÿ‡ฒ Zambia"), + ("๐Ÿ‡ฟ๐Ÿ‡ผ", "๐Ÿ‡ฟ๐Ÿ‡ผ Zimbabwe"), + ("๐Ÿ”ƒ", "๐Ÿ”ƒ Clockwise Arrows"), + ("๐Ÿ”„", "๐Ÿ”„ Anticlockwise Arrows"), + ("๐Ÿ”™", "๐Ÿ”™ Back"), + ("๐Ÿ”š", "๐Ÿ”š End"), + ("๐Ÿ”›", "๐Ÿ”› On"), + ("๐Ÿ”œ", "๐Ÿ”œ Soon"), + ("๐Ÿ”", "๐Ÿ” Top"), + ("๐Ÿ”ฐ", "๐Ÿ”ฐ Beginner"), + ("๐Ÿ”ฎ", "๐Ÿ”ฎ Crystal Ball"), + ("๐Ÿ”ฏ", "๐Ÿ”ฏ Six Pointed Star With Middle Dot"), + ("โœ…", "โœ… White Heavy Check Mark"), + ("โŒ", "โŒ Cross"), + ("โŽ", "โŽ Negative Squared Cross Mark"), + ("โž•", "โž• Heavy Plus Sign"), + ("โž–", "โž– Heavy Minus Sign"), + ("โž—", "โž— Heavy Division Sign"), + ("โžฐ", "โžฐ Curly Loop"), + ("โžฟ", "โžฟ Double Curly Loop"), + ("โ“", "โ“ Question"), + ("โ”", "โ” White Question Mark Ornament"), + ("โ•", "โ• White Exclamation Mark Ornament"), + ("๐Ÿ’ฏ", "๐Ÿ’ฏ Hundred Points"), + ("๐Ÿ”ž", "๐Ÿ”ž Over Eighteen"), + ("๐Ÿ” ", "๐Ÿ”  Latin Capital Letters"), + ("๐Ÿ”ก", "๐Ÿ”ก Latin Small Letters"), + ("๐Ÿ”ข", "๐Ÿ”ข Numbers"), + ("๐Ÿ”ฃ", "๐Ÿ”ฃ Symbols"), + ("๐Ÿ”ค", "๐Ÿ”ค Latin Letters"), + ("๐Ÿ…ฐ๏ธ", "๐Ÿ…ฐ๏ธ Squared A"), + ("๐Ÿ†Ž", "๐Ÿ†Ž Squared AB"), + ("๐Ÿ…ฑ๏ธ", "๐Ÿ…ฑ๏ธ Squared B"), + ("๐Ÿ†‘", "๐Ÿ†‘ Squared CL"), + ("๐Ÿ†’", "๐Ÿ†’ Cool Square"), + ("๐Ÿ†“", "๐Ÿ†“ Squared Free"), + ("๐Ÿ†”", "๐Ÿ†” Squared ID"), + ("๐Ÿ†•", "๐Ÿ†• New Square"), + ("๐Ÿ†–", "๐Ÿ†– Squared NG"), + ("๐Ÿ…พ๏ธ", "๐Ÿ…พ๏ธ Squared O"), + ("๐Ÿ†—", "๐Ÿ†— OK Square"), + ("๐Ÿ†˜", "๐Ÿ†˜ SOS Square"), + ("๐Ÿ†™", "๐Ÿ†™ Squared Up!"), + ("๐Ÿ†š", "๐Ÿ†š Squared Vs"), + ("๐Ÿˆ", "๐Ÿˆ Squared Katakana Koko"), + ("๐Ÿˆ‚๏ธ", "๐Ÿˆ‚๏ธ Squared Katakana Sa"), + ("๐Ÿˆท๏ธ", "๐Ÿˆท๏ธ Squared ๆœˆ (Moon)"), + ("๐Ÿˆถ", "๐Ÿˆถ Squared ๆœ‰ (Have)"), + ("๐Ÿ‰", "๐Ÿ‰ Circled Ideograph Advantage"), + ("๐Ÿˆน", "๐Ÿˆน Squared CJK Unified Ideograph-5272"), + ("๐Ÿˆฒ", "๐Ÿˆฒ Squared CJK Unified Ideograph-7981"), + ("๐Ÿ‰‘", "๐Ÿ‰‘ Circled ๅฏ (Accept)"), + ("๐Ÿˆธ", "๐Ÿˆธ Squared CJK Unified Ideograph-7533"), + ("๐Ÿˆด", "๐Ÿˆด Squared CJK Unified Ideograph-5408"), + ("๐Ÿˆณ", "๐Ÿˆณ Squared CJK Unified Ideograph-7a7a"), + ("๐Ÿˆบ", "๐Ÿˆบ Squared CJK Unified Ideograph-55b6"), + ("๐Ÿˆต", "๐Ÿˆต Squared CJK Unified Ideograph-6e80"), + ("๐Ÿ”ถ", "๐Ÿ”ถ Large Orange Diamond"), + ("๐Ÿ”ท", "๐Ÿ”ท Large Blue Diamond"), + ("๐Ÿ”ธ", "๐Ÿ”ธ Small Orange Diamond"), + ("๐Ÿ”น", "๐Ÿ”น Small Blue Diamond"), + ("๐Ÿ”บ", "๐Ÿ”บ Up-Pointing Red Triangle"), + ("๐Ÿ”ป", "๐Ÿ”ป Down-Pointing Red Triangle"), + ("๐Ÿ’ ", "๐Ÿ’  Diamond Shape With a Dot Inside"), + ("๐Ÿ”˜", "๐Ÿ”˜ Radio Button"), + ("๐Ÿ”ฒ", "๐Ÿ”ฒ Black Square Button"), + ("๐Ÿ”ณ", "๐Ÿ”ณ White Square Button"), + ("๐Ÿ”ด", "๐Ÿ”ด Large Red Circle"), + ("๐Ÿ”ต", "๐Ÿ”ต Large Blue Circle"), + ("๐Ÿ˜€", "๐Ÿ˜€ Grinning"), + ("๐Ÿ˜—", "๐Ÿ˜— Kissing"), + ("๐Ÿ˜™", "๐Ÿ˜™ Smooch"), + ("๐Ÿ˜‘", "๐Ÿ˜‘ True Neutral"), + ("๐Ÿ˜ฎ", "๐Ÿ˜ฎ Stunned"), + ("๐Ÿ˜ฏ", "๐Ÿ˜ฏ Hushed"), + ("๐Ÿ˜ด", "๐Ÿ˜ด Sleepy"), + ("๐Ÿ˜›", "๐Ÿ˜› Tongue"), + ("๐Ÿ˜•", "๐Ÿ˜• Confused"), + ("๐Ÿ˜Ÿ", "๐Ÿ˜Ÿ Worried"), + ("๐Ÿ˜ฆ", "๐Ÿ˜ฆ Frowning Face With Open Mouth"), + ("๐Ÿ˜ง", "๐Ÿ˜ง Anguish Face"), + ("๐Ÿ˜ฌ", "๐Ÿ˜ฌ Grimace"), + ("๐Ÿ™‚", "๐Ÿ™‚ Slightly Smiling"), + ("๐Ÿ™", "๐Ÿ™ Slightly Frowning"), + ("๐Ÿ•ต", "๐Ÿ•ต Spy"), + ("๐Ÿ—ฃ", "๐Ÿ—ฃ Speaking Head in Silhouette"), + ("๐Ÿ•ด", "๐Ÿ•ด Man in Business Suit Levitating"), + ("๐Ÿ–•", "๐Ÿ–• Middle Finger"), + ("๐Ÿ––", "๐Ÿ–– Vulcan Hand"), + ("๐Ÿ–", "๐Ÿ– Raised Hand With Fingers Splayed"), + ("๐Ÿ‘", "๐Ÿ‘ Eye"), + ("๐Ÿ•ณ", "๐Ÿ•ณ Hole"), + ("๐Ÿ—ฏ", "๐Ÿ—ฏ Right Anger Bubble"), + ("๐Ÿ•ถ", "๐Ÿ•ถ Sunglasses"), + ("๐Ÿ›", "๐Ÿ› Shopping"), + ("๐Ÿฟ", "๐Ÿฟ Chipmunk"), + ("๐Ÿ•Š", "๐Ÿ•Š Peace Dove"), + ("๐Ÿ•ท", "๐Ÿ•ท Spider"), + ("๐Ÿ•ธ", "๐Ÿ•ธ Spider Web"), + ("๐Ÿต", "๐Ÿต Rosette"), + ("๐ŸŒถ", "๐ŸŒถ Chilli"), + ("๐Ÿฝ", "๐Ÿฝ Fork and Knife With Plate"), + ("๐Ÿ—บ", "๐Ÿ—บ World Map"), + ("๐Ÿ”", "๐Ÿ” Snow Capped Mountain"), + ("๐Ÿ•", "๐Ÿ• Camping"), + ("๐Ÿ–", "๐Ÿ– Beach"), + ("๐Ÿœ", "๐Ÿœ Desert"), + ("๐Ÿ", "๐Ÿ Desert Island"), + ("๐Ÿž", "๐Ÿž National Park"), + ("๐ŸŸ", "๐ŸŸ Stadium"), + ("๐Ÿ›", "๐Ÿ› Architecture"), + ("๐Ÿ—", "๐Ÿ— Building Construction"), + ("๐Ÿ˜", "๐Ÿ˜ House Buildings"), + ("๐Ÿ™", "๐Ÿ™ Cityscape"), + ("๐Ÿš", "๐Ÿš Derelict House Building"), + ("๐Ÿ–ผ", "๐Ÿ–ผ Frame With Picture"), + ("๐Ÿ›ข", "๐Ÿ›ข Oil Drum"), + ("๐Ÿ›ฃ", "๐Ÿ›ฃ Motorway"), + ("๐Ÿ›ค", "๐Ÿ›ค Railway Track"), + ("๐Ÿ›ณ", "๐Ÿ›ณ Passenger Ship"), + ("๐Ÿ›ฅ", "๐Ÿ›ฅ Boat"), + ("๐Ÿ›ฉ", "๐Ÿ›ฉ Airplane"), + ("๐Ÿ›ซ", "๐Ÿ›ซ Airplane Departure"), + ("๐Ÿ›ฌ", "๐Ÿ›ฌ Airplane Arriving"), + ("๐Ÿ›ฐ", "๐Ÿ›ฐ Satellite"), + ("๐Ÿ›Ž", "๐Ÿ›Ž Service Bell"), + ("๐Ÿ›Œ", "๐Ÿ›Œ Bed"), + ("๐Ÿ›", "๐Ÿ› Bed"), + ("๐Ÿ›‹", "๐Ÿ›‹ Couch and Lamp"), + ("๐Ÿ•ฐ", "๐Ÿ•ฐ Mantelpiece"), + ("๐ŸŒก", "๐ŸŒก Thermometer"), + ("๐ŸŒค", "๐ŸŒค Small Cloud"), + ("๐ŸŒฅ", "๐ŸŒฅ White Sun Behind Cloud"), + ("๐ŸŒฆ", "๐ŸŒฆ White Sun Behind Cloud With Rain"), + ("๐ŸŒง", "๐ŸŒง Cloud With Rain"), + ("๐ŸŒจ", "๐ŸŒจ Cloud With Snow"), + ("๐ŸŒฉ", "๐ŸŒฉ Lightning"), + ("๐ŸŒช", "๐ŸŒช Tornado"), + ("๐ŸŒซ", "๐ŸŒซ Fog"), + ("๐ŸŒฌ", "๐ŸŒฌ Blowing"), + ("๐ŸŽ–", "๐ŸŽ– Medal"), + ("๐ŸŽ—", "๐ŸŽ— Ribbon"), + ("๐ŸŽž", "๐ŸŽž Film"), + ("๐ŸŽŸ", "๐ŸŽŸ Admission Tickets"), + ("๐Ÿท", "๐Ÿท Label"), + ("๐ŸŒ", "๐ŸŒ Golfer"), + ("๐Ÿ‹", "๐Ÿ‹ Lifting"), + ("๐ŸŽ", "๐ŸŽ Racing Car"), + ("๐Ÿ", "๐Ÿ Racing Motorcycle"), + ("๐Ÿ…", "๐Ÿ… Medal"), + ("๐Ÿ•น", "๐Ÿ•น Joystick"), + ("โธ", "โธ Double Vertical Bar"), + ("โน", "โน Black Square for Stop"), + ("โบ", "โบ Black Circle for Record"), + ("๐ŸŽ™", "๐ŸŽ™ Microphone"), + ("๐ŸŽš", "๐ŸŽš Level Slider"), + ("๐ŸŽ›", "๐ŸŽ› Control Knobs"), + ("๐Ÿ–ฅ", "๐Ÿ–ฅ Desktop"), + ("๐Ÿ–จ", "๐Ÿ–จ Printer"), + ("๐Ÿ–ฑ", "๐Ÿ–ฑ Three Button Mouse"), + ("๐Ÿ–ฒ", "๐Ÿ–ฒ Trackball"), + ("๐Ÿ“ฝ", "๐Ÿ“ฝ Film Projector"), + ("๐Ÿ“ธ", "๐Ÿ“ธ Camera With Flash"), + ("๐Ÿ•ฏ", "๐Ÿ•ฏ Candle"), + ("๐Ÿ—ž", "๐Ÿ—ž Newspaper"), + ("๐Ÿ—ณ", "๐Ÿ—ณ Ballot Box With Ballot"), + ("๐Ÿ–‹", "๐Ÿ–‹ Fancy Pen"), + ("๐Ÿ–Š", "๐Ÿ–Š Lower Left Ballpoint Pen"), + ("๐Ÿ–Œ", "๐Ÿ–Œ Lower Left Paintbrush"), + ("๐Ÿ–", "๐Ÿ– Lower Left Crayon"), + ("๐Ÿ—‚", "๐Ÿ—‚ Card Index Dividers"), + ("๐Ÿ—’", "๐Ÿ—’ Spiral Note Pad"), + ("๐Ÿ—“", "๐Ÿ—“ Spiral Calendar Pad"), + ("๐Ÿ–‡", "๐Ÿ–‡ Linked Paperclips"), + ("๐Ÿ—ƒ", "๐Ÿ—ƒ Card File Box"), + ("๐Ÿ—„", "๐Ÿ—„ File Cabinet"), + ("๐Ÿ—‘", "๐Ÿ—‘ Wastebasket"), + ("๐Ÿ—", "๐Ÿ— Old Key"), + ("๐Ÿ› ", "๐Ÿ›  Tools"), + ("๐Ÿ—œ", "๐Ÿ—œ Compression"), + ("๐Ÿ—ก", "๐Ÿ—ก Dagger"), + ("๐Ÿ›ก", "๐Ÿ›ก Shield"), + ("๐Ÿณ", "๐Ÿณ White Flag"), + ("๐Ÿด", "๐Ÿด Black Flag"), + ("๐Ÿ•‰", "๐Ÿ•‰ Om Symbol"), + ("๐Ÿ—จ", "๐Ÿ—จ Left Speech Bubble"), + ("๐Ÿค—", "๐Ÿค— Hugging"), + ("๐Ÿค”", "๐Ÿค” Thinking"), + ("๐Ÿ™„", "๐Ÿ™„ Rolling Eyes"), + ("๐Ÿค", "๐Ÿค Hushed"), + ("๐Ÿค“", "๐Ÿค“ Nerd"), + ("๐Ÿ™ƒ", "๐Ÿ™ƒ Upside Down"), + ("๐Ÿค’", "๐Ÿค’ Sick"), + ("๐Ÿค•", "๐Ÿค• Hurt Head"), + ("๐Ÿค‘", "๐Ÿค‘ Money"), + ("๐Ÿป", "๐Ÿป Emoji Modifier 1-2"), + ("๐Ÿผ", "๐Ÿผ Emoji Modifier 3"), + ("๐Ÿฝ", "๐Ÿฝ Emoji Modifier 4"), + ("๐Ÿพ", "๐Ÿพ Emoji Modifier 5"), + ("๐Ÿฟ", "๐Ÿฟ Emoji Modifier 6"), + ("๐Ÿค˜", "๐Ÿค˜ Rock On"), + ("๐Ÿ“ฟ", "๐Ÿ“ฟ Prayer Beads"), + ("๐Ÿค–", "๐Ÿค– Robot"), + ("๐Ÿฆ", "๐Ÿฆ Lion"), + ("๐Ÿฆ„", "๐Ÿฆ„ Unicorn"), + ("๐Ÿฆƒ", "๐Ÿฆƒ Turkey"), + ("๐Ÿฆ€", "๐Ÿฆ€ Crab"), + ("๐Ÿฆ‚", "๐Ÿฆ‚ Scorpion"), + ("๐Ÿง€", "๐Ÿง€ Cheese"), + ("๐ŸŒญ", "๐ŸŒญ Hot Dog"), + ("๐ŸŒฎ", "๐ŸŒฎ Taco"), + ("๐ŸŒฏ", "๐ŸŒฏ Burrito"), + ("๐Ÿฟ", "๐Ÿฟ Popcorn"), + ("๐Ÿพ", "๐Ÿพ Popping Cork"), + ("๐Ÿบ", "๐Ÿบ Amphora"), + ("๐Ÿ›", "๐Ÿ› Place of Worship"), + ("๐Ÿ•‹", "๐Ÿ•‹ Kaaba"), + ("๐Ÿ•Œ", "๐Ÿ•Œ Mosque"), + ("๐Ÿ•", "๐Ÿ• Synagogue"), + ("๐Ÿ•Ž", "๐Ÿ•Ž Menorah"), + ("๐Ÿ", "๐Ÿ Bat and Ball"), + ("๐Ÿ", "๐Ÿ Volleyball"), + ("๐Ÿ‘", "๐Ÿ‘ Field Hockey"), + ("๐Ÿ’", "๐Ÿ’ Ice Hockey"), + ("๐Ÿ“", "๐Ÿ“ Table Tennis"), + ("๐Ÿธ", "๐Ÿธ Badminton"), + ("๐Ÿน", "๐Ÿน Archer"), + ("๐Ÿคฃ", "๐Ÿคฃ ROFL Face"), + ("๐Ÿคค", "๐Ÿคค Drooling"), + ("๐Ÿคข", "๐Ÿคข Nauseated"), + ("๐Ÿคง", "๐Ÿคง Sneezing"), + ("๐Ÿค ", "๐Ÿค  Cowboy"), + ("๐Ÿคก", "๐Ÿคก Clown"), + ("๐Ÿคฅ", "๐Ÿคฅ Lying"), + ("๐Ÿคด", "๐Ÿคด Prince"), + ("๐Ÿคต", "๐Ÿคต Tuxedo Man"), + ("๐Ÿคฐ", "๐Ÿคฐ Pregnant"), + ("๐Ÿคถ", "๐Ÿคถ Mrs. Claus"), + ("๐Ÿคฆ", "๐Ÿคฆ Facepalm"), + ("๐Ÿคท", "๐Ÿคท Shrugging"), + ("๐Ÿ•บ", "๐Ÿ•บ Man Dancing"), + ("๐Ÿคบ", "๐Ÿคบ Fencing"), + ("๐Ÿคธ", "๐Ÿคธ Cartwheel"), + ("๐Ÿคผ", "๐Ÿคผ Wrestling"), + ("๐Ÿคฝ", "๐Ÿคฝ Water Polo"), + ("๐Ÿคพ", "๐Ÿคพ Handball"), + ("๐Ÿคน", "๐Ÿคน Juggling"), + ("๐Ÿคณ", "๐Ÿคณ Selfie"), + ("๐Ÿคž", "๐Ÿคž Luck Hand"), + ("๐Ÿค™", "๐Ÿค™ Call Me Hand"), + ("๐Ÿค›", "๐Ÿค› Left-Facing Fist"), + ("๐Ÿคœ", "๐Ÿคœ Right-Facing Fist"), + ("๐Ÿคš", "๐Ÿคš Raised Back of Hand"), + ("๐Ÿค", "๐Ÿค Business Hi"), + ("๐Ÿ–ค", "๐Ÿ–ค Black Heart"), + ("๐Ÿฆ", "๐Ÿฆ Gorilla"), + ("๐ŸฆŠ", "๐ŸฆŠ Fox"), + ("๐ŸฆŒ", "๐ŸฆŒ Deer"), + ("๐Ÿฆ", "๐Ÿฆ Rhinoceros"), + ("๐Ÿฆ‡", "๐Ÿฆ‡ Bat"), + ("๐Ÿฆ…", "๐Ÿฆ… Eagle"), + ("๐Ÿฆ†", "๐Ÿฆ† Duck"), + ("๐Ÿฆ‰", "๐Ÿฆ‰ Owl"), + ("๐ŸฆŽ", "๐ŸฆŽ Lizard"), + ("๐Ÿฆˆ", "๐Ÿฆˆ Shark"), + ("๐Ÿฆ", "๐Ÿฆ Shrimp"), + ("๐Ÿฆ‘", "๐Ÿฆ‘ Squid"), + ("๐Ÿฆ‹", "๐Ÿฆ‹ Butterfly"), + ("๐Ÿฅ€", "๐Ÿฅ€ Wilted"), + ("๐Ÿฅ", "๐Ÿฅ Kiwifruit"), + ("๐Ÿฅ‘", "๐Ÿฅ‘ Pricey Fruit"), + ("๐Ÿฅ”", "๐Ÿฅ” Potato"), + ("๐Ÿฅ•", "๐Ÿฅ• Carrot"), + ("๐Ÿฅ’", "๐Ÿฅ’ Cucumber"), + ("๐Ÿฅœ", "๐Ÿฅœ Peanuts"), + ("๐Ÿฅ", "๐Ÿฅ Croissant"), + ("๐Ÿฅ–", "๐Ÿฅ– Bread Sword"), + ("๐Ÿฅž", "๐Ÿฅž Pancakes"), + ("๐Ÿฅ“", "๐Ÿฅ“ Bacon"), + ("๐Ÿฅ™", "๐Ÿฅ™ Stuffed Flatbread"), + ("๐Ÿฅš", "๐Ÿฅš Chicken Rock"), + ("๐Ÿฅ˜", "๐Ÿฅ˜ Shallow Pan"), + ("๐Ÿฅ—", "๐Ÿฅ— Salad"), + ("๐Ÿฅ›", "๐Ÿฅ› Cow Juice"), + ("๐Ÿฅ‚", "๐Ÿฅ‚ Clinking Glasses"), + ("๐Ÿฅƒ", "๐Ÿฅƒ Tumbler"), + ("๐Ÿฅ„", "๐Ÿฅ„ Spoon"), + ("๐Ÿ›ด", "๐Ÿ›ด Scoot Scoot"), + ("๐Ÿ›ต", "๐Ÿ›ต Motor Scooter"), + ("๐Ÿ›‘", "๐Ÿ›‘ Stop Sign"), + ("๐Ÿ›ถ", "๐Ÿ›ถ Canoe"), + ("๐Ÿฅ‡", "๐Ÿฅ‡ Gold Medal"), + ("๐Ÿฅˆ", "๐Ÿฅˆ Silver Medal"), + ("๐Ÿฅ‰", "๐Ÿฅ‰ Participation"), + ("๐ŸฅŠ", "๐ŸฅŠ Boxing"), + ("๐Ÿฅ‹", "๐Ÿฅ‹ Martial Arts"), + ("๐Ÿฅ…", "๐Ÿฅ… Hashtag Goals"), + ("๐Ÿฅ", "๐Ÿฅ Drum Roll"), + ("๐Ÿ›’", "๐Ÿ›’ Food Ute"), + ("๐Ÿคฉ", "๐Ÿคฉ Star Struck"), + ("๐Ÿคจ", "๐Ÿคจ Unexpected Face"), + ("๐Ÿคฏ", "๐Ÿคฏ Mind Blown"), + ("๐Ÿคช", "๐Ÿคช Zany Face"), + ("๐Ÿคฌ", "๐Ÿคฌ Swear Face"), + ("๐Ÿคฎ", "๐Ÿคฎ Vomiting"), + ("๐Ÿคซ", "๐Ÿคซ Shushing"), + ("๐Ÿคญ", "๐Ÿคญ Hand Over Mouth"), + ("๐Ÿง", "๐Ÿง Monocle"), + ("๐Ÿง’", "๐Ÿง’ Child Face"), + ("๐Ÿง‘", "๐Ÿง‘ Adult"), + ("๐Ÿง“", "๐Ÿง“ Older Adult"), + ("๐Ÿง•", "๐Ÿง• Headscarf"), + ("๐Ÿง”", "๐Ÿง” Bearded Person"), + ("๐Ÿคฑ", "๐Ÿคฑ Breast Feeding"), + ("๐Ÿง™", "๐Ÿง™ Mage"), + ("๐Ÿงš", "๐Ÿงš Fairy"), + ("๐Ÿง›", "๐Ÿง› Vampire"), + ("๐Ÿงœ", "๐Ÿงœ Merperson"), + ("๐Ÿง", "๐Ÿง Cosplay"), + ("๐Ÿงž", "๐Ÿงž Genie"), + ("๐ŸงŸ", "๐ŸงŸ Unalive"), + ("๐Ÿง–", "๐Ÿง– Steamy Room"), + ("๐Ÿง—", "๐Ÿง— Person Climbing"), + ("๐Ÿง˜", "๐Ÿง˜ Lotus Position"), + ("๐ŸคŸ", "๐ŸคŸ Love-You Gesture"), + ("๐Ÿคฒ", "๐Ÿคฒ Palms Up Together"), + ("๐Ÿง ", "๐Ÿง  Big Brain"), + ("๐Ÿงก", "๐Ÿงก Orange Heart"), + ("๐Ÿงฃ", "๐Ÿงฃ Neck Hider"), + ("๐Ÿงค", "๐Ÿงค Hand Socks"), + ("๐Ÿงฅ", "๐Ÿงฅ Coat"), + ("๐Ÿงฆ", "๐Ÿงฆ Feet Gloves"), + ("๐Ÿงข", "๐Ÿงข Billed Cap"), + ("๐Ÿฆ“", "๐Ÿฆ“ Zebra"), + ("๐Ÿฆ’", "๐Ÿฆ’ Giraffe"), + ("๐Ÿฆ”", "๐Ÿฆ” Spikehog"), + ("๐Ÿฆ•", "๐Ÿฆ• Long Neck"), + ("๐Ÿฆ–", "๐Ÿฆ– Big Roar"), + ("๐Ÿฆ—", "๐Ÿฆ— Cricket"), + ("๐Ÿฅฅ", "๐Ÿฅฅ Coconut"), + ("๐Ÿฅฆ", "๐Ÿฅฆ Tiny Tree"), + ("๐Ÿฅจ", "๐Ÿฅจ Twisty Bread"), + ("๐Ÿฅฉ", "๐Ÿฅฉ Cut of Meat"), + ("๐Ÿฅช", "๐Ÿฅช Sandwich"), + ("๐Ÿฅฃ", "๐Ÿฅฃ Bowl With Spoon"), + ("๐Ÿฅซ", "๐Ÿฅซ Canned Good"), + ("๐ŸฅŸ", "๐ŸฅŸ Dumpling"), + ("๐Ÿฅ ", "๐Ÿฅ  Tasty Future"), + ("๐Ÿฅก", "๐Ÿฅก Takeout Box"), + ("๐Ÿฅง", "๐Ÿฅง Pie"), + ("๐Ÿฅค", "๐Ÿฅค Cup With Straw"), + ("๐Ÿฅข", "๐Ÿฅข Chopsticks"), + ("๐Ÿ›ธ", "๐Ÿ›ธ Alien Plane"), + ("๐Ÿ›ท", "๐Ÿ›ท Sled"), + ("๐ŸฅŒ", "๐ŸฅŒ Curling"), + ("๐Ÿฅฐ", "๐Ÿฅฐ Smiling Face With 3 Hearts"), + ("๐Ÿฅต", "๐Ÿฅต Overheated"), + ("๐Ÿฅถ", "๐Ÿฅถ Freezing Face"), + ("๐Ÿฅด", "๐Ÿฅด Woozy Face"), + ("๐Ÿฅณ", "๐Ÿฅณ Party Face"), + ("๐Ÿฅบ", "๐Ÿฅบ Pleading Face"), + ("๐Ÿฆต", "๐Ÿฆต Leg"), + ("๐Ÿฆถ", "๐Ÿฆถ Foot"), + ("๐Ÿฆท", "๐Ÿฆท Tooth"), + ("๐Ÿฆด", "๐Ÿฆด Bone"), + ("๐Ÿฆธ", "๐Ÿฆธ Superhero"), + ("๐Ÿฆน", "๐Ÿฆน Supervillain"), + ("๐Ÿฆ", "๐Ÿฆ Trash Bandit"), + ("๐Ÿฆ™", "๐Ÿฆ™ Llama"), + ("๐Ÿฆ›", "๐Ÿฆ› Hippopotamus"), + ("๐Ÿฆ˜", "๐Ÿฆ˜ Kangaroo"), + ("๐Ÿฆก", "๐Ÿฆก Badger"), + ("๐Ÿฆข", "๐Ÿฆข Swan"), + ("๐Ÿฆš", "๐Ÿฆš Peacock"), + ("๐Ÿฆœ", "๐Ÿฆœ Parrot"), + ("๐ŸฆŸ", "๐ŸฆŸ Mosquito"), + ("๐Ÿฆ ", "๐Ÿฆ  Microbe"), + ("๐Ÿฅญ", "๐Ÿฅญ Mango"), + ("๐Ÿฅฌ", "๐Ÿฅฌ Leafy Green"), + ("๐Ÿฅฏ", "๐Ÿฅฏ Bagel"), + ("๐Ÿง‚", "๐Ÿง‚ Salty"), + ("๐Ÿฅฎ", "๐Ÿฅฎ Moon Cake"), + ("๐Ÿฆž", "๐Ÿฆž Lobster"), + ("๐Ÿง", "๐Ÿง Cupcake"), + ("๐Ÿงญ", "๐Ÿงญ Compass"), + ("๐Ÿงฑ", "๐Ÿงฑ Brick"), + ("๐Ÿ›น", "๐Ÿ›น Skateboard"), + ("๐Ÿงณ", "๐Ÿงณ Baggage"), + ("๐Ÿงจ", "๐Ÿงจ Firework"), + ("๐Ÿงง", "๐Ÿงง Red Envelope"), + ("๐ŸฅŽ", "๐ŸฅŽ Softball"), + ("๐Ÿฅ", "๐Ÿฅ Throwing Disc"), + ("๐Ÿฅ", "๐Ÿฅ Lacrosse"), + ("๐Ÿงฟ", "๐Ÿงฟ Nazar Amulet"), + ("๐Ÿงฉ", "๐Ÿงฉ Puzzle Piece"), + ("๐Ÿงธ", "๐Ÿงธ Teddy Bear"), + ("๐Ÿงต", "๐Ÿงต Thread"), + ("๐Ÿงถ", "๐Ÿงถ Yarn Ball"), + ("๐Ÿฅฝ", "๐Ÿฅฝ The Goggles"), + ("๐Ÿฅผ", "๐Ÿฅผ Lab Coat"), + ("๐Ÿฅพ", "๐Ÿฅพ Hiking Boot"), + ("๐Ÿฅฟ", "๐Ÿฅฟ Flat Shoe"), + ("๐Ÿงฎ", "๐Ÿงฎ Abacus"), + ("๐Ÿงพ", "๐Ÿงพ Receipt"), + ("๐Ÿงฐ", "๐Ÿงฐ Toolbox"), + ("๐Ÿงฒ", "๐Ÿงฒ Magnet"), + ("๐Ÿงช", "๐Ÿงช Test Tube"), + ("๐Ÿงซ", "๐Ÿงซ Petri Dish"), + ("๐Ÿงฌ", "๐Ÿงฌ DNA"), + ("๐Ÿงด", "๐Ÿงด Lotion"), + ("๐Ÿงท", "๐Ÿงท Safety Pin"), + ("๐Ÿงน", "๐Ÿงน Broom"), + ("๐Ÿงบ", "๐Ÿงบ Basket"), + ("๐Ÿงป", "๐Ÿงป Roll of Paper"), + ("๐Ÿงผ", "๐Ÿงผ Soap"), + ("๐Ÿงฝ", "๐Ÿงฝ Fun sponge"), + ("๐Ÿงฏ", "๐Ÿงฏ Anti-fire Can"), + ("๐Ÿฅฑ", "๐Ÿฅฑ Yawning Face"), + ("๐ŸคŽ", "๐ŸคŽ Brown Heart"), + ("๐Ÿค", "๐Ÿค White Heart"), + ("๐Ÿค", "๐Ÿค Pinching Hand"), + ("๐Ÿฆพ", "๐Ÿฆพ Mechanical Arm"), + ("๐Ÿฆฟ", "๐Ÿฆฟ Mechanical Leg"), + ("๐Ÿฆป", "๐Ÿฆป Ear with Hearing Aid"), + ("๐Ÿง", "๐Ÿง Deaf Person"), + ("๐Ÿง", "๐Ÿง Person Standing"), + ("๐ŸงŽ", "๐ŸงŽ Person Kneeling"), + ("๐Ÿฆง", "๐Ÿฆง Orangutan"), + ("๐Ÿฆฎ", "๐Ÿฆฎ Guide Dog"), + ("๐Ÿฆฅ", "๐Ÿฆฅ Lazy Tree Dog"), + ("๐Ÿฆฆ", "๐Ÿฆฆ Water Dog"), + ("๐Ÿฆจ", "๐Ÿฆจ Stinky dog"), + ("๐Ÿฆฉ", "๐Ÿฆฉ Pink Dog"), + ("๐Ÿง„", "๐Ÿง„ Garlic"), + ("๐Ÿง…", "๐Ÿง… Onion"), + ("๐Ÿง‡", "๐Ÿง‡ Waffle"), + ("๐Ÿง†", "๐Ÿง† Falafel"), + ("๐Ÿงˆ", "๐Ÿงˆ Butter"), + ("๐Ÿฆช", "๐Ÿฆช Oyster"), + ("๐Ÿงƒ", "๐Ÿงƒ Beverage Box"), + ("๐Ÿง‰", "๐Ÿง‰ Mate"), + ("๐ŸงŠ", "๐ŸงŠ Cold Cuboid"), + ("๐Ÿ›•", "๐Ÿ›• Hindu Temple"), + ("๐Ÿฆฝ", "๐Ÿฆฝ Manual Wheelchair"), + ("๐Ÿฆผ", "๐Ÿฆผ Motorized Wheelchair"), + ("๐Ÿ›บ", "๐Ÿ›บ Auto Rickshaw"), + ("๐Ÿช‚", "๐Ÿช‚ Parachute"), + ("๐Ÿช", "๐Ÿช Ringed Planet"), + ("๐Ÿคฟ", "๐Ÿคฟ Diving Mask"), + ("๐Ÿช€", "๐Ÿช€ Yo-Yo"), + ("๐Ÿช", "๐Ÿช Kite"), + ("๐Ÿฆบ", "๐Ÿฆบ Safety Vest"), + ("๐Ÿฅป", "๐Ÿฅป Sari"), + ("๐Ÿฉฑ", "๐Ÿฉฑ One-Piece Swimsuit"), + ("๐Ÿฉฒ", "๐Ÿฉฒ Briefs"), + ("๐Ÿฉณ", "๐Ÿฉณ Shorts"), + ("๐Ÿฉฐ", "๐Ÿฉฐ Ballet Shoes"), + ("๐Ÿช•", "๐Ÿช• Banjo"), + ("๐Ÿช”", "๐Ÿช” Diya Lamp"), + ("๐Ÿช“", "๐Ÿช“ Axe"), + ("๐Ÿฆฏ", "๐Ÿฆฏ White Cane"), + ("๐Ÿฉธ", "๐Ÿฉธ Drop of Blood"), + ("๐Ÿฉน", "๐Ÿฉน Adhesive Bandage"), + ("๐Ÿฉบ", "๐Ÿฉบ Stethoscope"), + ("๐Ÿช‘", "๐Ÿช‘ Chair"), + ("๐Ÿช’", "๐Ÿช’ Razor"), + ("๐ŸŸ ", "๐ŸŸ  Orange Circle"), + ("๐ŸŸก", "๐ŸŸก Yellow Circle"), + ("๐ŸŸข", "๐ŸŸข Green Circle"), + ("๐ŸŸฃ", "๐ŸŸฃ Purple Circle"), + ("๐ŸŸค", "๐ŸŸค Brown Circle"), + ("๐ŸŸฅ", "๐ŸŸฅ Red Square"), + ("๐ŸŸง", "๐ŸŸง Orange Square"), + ("๐ŸŸจ", "๐ŸŸจ Yellow Square"), + ("๐ŸŸฉ", "๐ŸŸฉ Green Square"), + ("๐ŸŸฆ", "๐ŸŸฆ Blue Square"), + ("๐ŸŸช", "๐ŸŸช Purple Square"), + ("๐ŸŸซ", "๐ŸŸซ Brown Square"), + ("๐Ÿฅฒ", "๐Ÿฅฒ Smiling Face with Tear"), + ("๐Ÿฅธ", "๐Ÿฅธ Disguised Face"), + ("๐ŸคŒ", "๐ŸคŒ Pinched Fingers"), + ("๐Ÿซ€", "๐Ÿซ€ Anatomical Heart"), + ("๐Ÿซ", "๐Ÿซ Lungs"), + ("๐Ÿฅท", "๐Ÿฅท Ninja"), + ("๐Ÿซ‚", "๐Ÿซ‚ People Hugging"), + ("๐Ÿฆฌ", "๐Ÿฆฌ Bison"), + ("๐Ÿฆฃ", "๐Ÿฆฃ Mammoth"), + ("๐Ÿฆซ", "๐Ÿฆซ Beaver"), + ("๐Ÿฆค", "๐Ÿฆค Dodo"), + ("๐Ÿชถ", "๐Ÿชถ Feather"), + ("๐Ÿฆญ", "๐Ÿฆญ Seal"), + ("๐Ÿชฒ", "๐Ÿชฒ Beetle"), + ("๐Ÿชณ", "๐Ÿชณ Cockroach"), + ("๐Ÿชฐ", "๐Ÿชฐ Fly"), + ("๐Ÿชฑ", "๐Ÿชฑ Worm"), + ("๐Ÿชด", "๐Ÿชด Potted Plant"), + ("๐Ÿซ", "๐Ÿซ Blueberries"), + ("๐Ÿซ’", "๐Ÿซ’ Olive"), + ("๐Ÿซ‘", "๐Ÿซ‘ Bell Pepper"), + ("๐Ÿซ“", "๐Ÿซ“ Flatbread"), + ("๐Ÿซ”", "๐Ÿซ” Tamale"), + ("๐Ÿซ•", "๐Ÿซ• Fondue"), + ("๐Ÿซ–", "๐Ÿซ– Teapot"), + ("๐Ÿง‹", "๐Ÿง‹ Bubble Tea"), + ("๐Ÿชจ", "๐Ÿชจ Rock"), + ("๐Ÿชต", "๐Ÿชต Wood"), + ("๐Ÿ›–", "๐Ÿ›– Hut"), + ("๐Ÿ›ป", "๐Ÿ›ป Pickup Truck"), + ("๐Ÿ›ผ", "๐Ÿ›ผ Roller Skate"), + ("๐Ÿช„", "๐Ÿช„ Magic Wand"), + ("๐Ÿช…", "๐Ÿช… Piรฑata"), + ("๐Ÿช†", "๐Ÿช† Nesting Dolls"), + ("๐Ÿชก", "๐Ÿชก Sewing Needle"), + ("๐Ÿชข", "๐Ÿชข Knot"), + ("๐Ÿฉด", "๐Ÿฉด Thong Sandal"), + ("๐Ÿช–", "๐Ÿช– Military Helmet"), + ("๐Ÿช—", "๐Ÿช— Accordion"), + ("๐Ÿช˜", "๐Ÿช˜ Long Drum"), + ("๐Ÿช™", "๐Ÿช™ Coin"), + ("๐Ÿชƒ", "๐Ÿชƒ Boomerang"), + ("๐Ÿชš", "๐Ÿชš Carpentry Saw"), + ("๐Ÿช›", "๐Ÿช› Screwdriver"), + ("๐Ÿช", "๐Ÿช Hook"), + ("๐Ÿชœ", "๐Ÿชœ Ladder"), + ("๐Ÿ›—", "๐Ÿ›— Elevator"), + ("๐Ÿชž", "๐Ÿชž Mirror"), + ("๐ŸชŸ", "๐ŸชŸ Window"), + ("๐Ÿช ", "๐Ÿช  Plunger"), + ("๐Ÿชค", "๐Ÿชค Mouse Trap"), + ("๐Ÿชฃ", "๐Ÿชฃ Bucket"), + ("๐Ÿชฅ", "๐Ÿชฅ Toothbrush"), + ("๐Ÿชฆ", "๐Ÿชฆ Headstone"), + ("๐Ÿชง", "๐Ÿชง Placard"), + ("๐Ÿ˜ถ\u200d๐ŸŒซ", "๐Ÿ˜ถ\u200d๐ŸŒซ Cloudy Face"), + ("๐Ÿ˜ฎ\u200d๐Ÿ’จ", "๐Ÿ˜ฎ\u200d๐Ÿ’จ Hot Air"), + ("๐Ÿ˜ต\u200d๐Ÿ’ซ", "๐Ÿ˜ต\u200d๐Ÿ’ซ Hypnotised"), + ("โค\u200d๐Ÿ”ฅ", "โค\u200d๐Ÿ”ฅ Fiery Heart"), + ("โค\u200d๐Ÿฉน", "โค\u200d๐Ÿฉน Mending Heart"), + ("๐Ÿง”\u200dโ™‚", "๐Ÿง”\u200dโ™‚ Bearded Man"), + ("๐Ÿง”\u200dโ™€", "๐Ÿง”\u200dโ™€ Bearded Woman"), + ("๐Ÿซ ", "๐Ÿซ  Melting Face"), + ("๐Ÿซข", "๐Ÿซข Oops Face"), + ("๐Ÿซฃ", "๐Ÿซฃ Peekaboo"), + ("๐Ÿซก", "๐Ÿซก Saluting Face"), + ("๐Ÿซฅ", "๐Ÿซฅ Invisible Face"), + ("๐Ÿซค", "๐Ÿซค Diagonal Mouth"), + ("๐Ÿฅน", "๐Ÿฅน Grateful Face"), + ("๐Ÿซฑ", "๐Ÿซฑ Rightwards Hand"), + ("๐Ÿซฒ", "๐Ÿซฒ Leftwards Hand"), + ("๐Ÿซณ", "๐Ÿซณ Palm Down Hand"), + ("๐Ÿซด", "๐Ÿซด Palm Up Hand"), + ("๐Ÿซฐ", "๐Ÿซฐ Love Gesture"), + ("๐Ÿซต", "๐Ÿซต YOU"), + ("๐Ÿซถ", "๐Ÿซถ Heart Hands"), + ("๐Ÿซฆ", "๐Ÿซฆ Biting Lip"), + ("๐Ÿซ…", "๐Ÿซ… Crowned"), + ("๐Ÿซƒ", "๐Ÿซƒ Pregnant Man"), + ("๐Ÿซ„", "๐Ÿซ„ Pregnant Person"), + ("๐ŸงŒ", "๐ŸงŒ Bridgekeeper"), + ("๐Ÿชธ", "๐Ÿชธ Coral"), + ("๐Ÿชท", "๐Ÿชท Lotus"), + ("๐Ÿชน", "๐Ÿชน Empty Nest"), + ("๐Ÿชบ", "๐Ÿชบ Unladen Swallow"), + ("๐Ÿซ˜", "๐Ÿซ˜ Beans"), + ("๐Ÿซ—", "๐Ÿซ— Leak"), + ("๐Ÿซ™", "๐Ÿซ™ Jar"), + ("๐Ÿ›", "๐Ÿ› Slide"), + ("๐Ÿ›ž", "๐Ÿ›ž Wheel"), + ("๐Ÿ›Ÿ", "๐Ÿ›Ÿ Buoy"), + ("๐Ÿชฉ", "๐Ÿชฉ Mirror Ball"), + ("๐Ÿชซ", "๐Ÿชซ Low Battery"), + ("๐Ÿฉผ", "๐Ÿฉผ Crutch"), + ("๐Ÿฉป", "๐Ÿฉป X-Ray"), + ("๐Ÿซง", "๐Ÿซง Bubbles"), + ("๐Ÿชฌ", "๐Ÿชฌ Hamsa"), + ("๐Ÿชช", "๐Ÿชช Identification Card"), + ("๐ŸŸฐ", "๐ŸŸฐ Heavy Equals Sign"), + ("\U0001fae8", "\U0001fae8 Car Sick Face"), + ("\U0001fa77", "\U0001fa77 Pink Heart"), + ("\U0001fa75", "\U0001fa75 Light Blue Heart"), + ("\U0001fa76", "\U0001fa76 Grey Heart"), + ("\U0001faf7", "\U0001faf7 No Thanks Hand"), + ("\U0001faf8", "\U0001faf8 Rightwards Pushing Hand"), + ("\U0001face", "\U0001face Moose"), + ("\U0001facf", "\U0001facf Donkey"), + ("\U0001fabd", "\U0001fabd Wing"), + ("\U0001fabf", "\U0001fabf Honking Bird"), + ("\U0001fabc", "\U0001fabc Jellyfish"), + ("\U0001fabb", "\U0001fabb Hyacinth"), + ("\U0001fada", "\U0001fada Ginger"), + ("\U0001fadb", "\U0001fadb Pea Pod"), + ("\U0001faad", "\U0001faad Folding Hand Fan"), + ("\U0001faae", "\U0001faae Hair Pick"), + ("\U0001fa87", "\U0001fa87 Maracas"), + ("\U0001fa88", "\U0001fa88 Flute"), + ("\U0001faaf", "\U0001faaf Khanda"), + ("\U0001f6dc", "\U0001f6dc Wireless"), + ("๐Ÿ™‚\u200dโ†”", "๐Ÿ™‚\u200dโ†” Headshake"), + ("๐Ÿ™‚\u200dโ†•", "๐Ÿ™‚\u200dโ†• Nodding Face"), + ("๐Ÿšถ\u200dโžก", "๐Ÿšถ\u200dโžก Walking"), + ("๐ŸงŽ\u200dโžก", "๐ŸงŽ\u200dโžก Person Kneeling"), + ("๐Ÿƒ\u200dโžก", "๐Ÿƒ\u200dโžก Person Running"), + ("๐Ÿฆ\u200d๐Ÿ”ฅ", "๐Ÿฆ\u200d๐Ÿ”ฅ Phoenix"), + ("๐Ÿ‹\u200d๐ŸŸฉ", "๐Ÿ‹\u200d๐ŸŸฉ Lime"), + ("๐Ÿ„\u200d๐ŸŸซ", "๐Ÿ„\u200d๐ŸŸซ Brown Mushroom"), + ("โ›“\u200d๐Ÿ’ฅ", "โ›“\u200d๐Ÿ’ฅ Broken Chain"), + ], + default=None, + max_length=3, + null=True, + verbose_name="emoji", + ), + ), + ] diff --git a/tabbycat/tournaments/migrations/0011_alter_round_unique_together_and_more.py b/tabbycat/tournaments/migrations/0012_alter_round_unique_together_and_more.py similarity index 92% rename from tabbycat/tournaments/migrations/0011_alter_round_unique_together_and_more.py rename to tabbycat/tournaments/migrations/0012_alter_round_unique_together_and_more.py index 7f45645e93f..28843826ede 100644 --- a/tabbycat/tournaments/migrations/0011_alter_round_unique_together_and_more.py +++ b/tabbycat/tournaments/migrations/0012_alter_round_unique_together_and_more.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ ('breakqual', '0006_alter_breakcategory_unique_together_and_more'), - ('tournaments', '0010_alter_round_draw_type'), + ('tournaments', '0011_alter_round_starts_at'), ] operations = [