From 4a2ac1d9d9422dcd4b3f9c8fd145c7a817521a67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Fri, 29 Nov 2024 18:24:08 -0500 Subject: [PATCH 1/3] Create function for saving sub-serializers There is a common pattern for creating/updating objects in the API for nested objects, overriding `validated_data`. This commit creates a helper function to avoid repeating. This also fixes failures caused by not overriding the "_errors" attribute. --- tabbycat/api/serializers.py | 129 ++++++++++++------------------------ 1 file changed, 41 insertions(+), 88 deletions(-) diff --git a/tabbycat/api/serializers.py b/tabbycat/api/serializers.py index 65ee9064984..780c7567054 100644 --- a/tabbycat/api/serializers.py +++ b/tabbycat/api/serializers.py @@ -50,6 +50,13 @@ def _validate_field(self, field, value): return value +def save_related(serializer, data, context, save_fields): + s = serializer(many=isinstance(data, list), context=context) + s._validated_data = data + s._errors = [] + s.save(**save_fields) + + class RootSerializer(serializers.Serializer): class RootLinksSerializer(serializers.Serializer): v1 = serializers.HyperlinkedIdentityField(view_name='api-v1-root') @@ -238,13 +245,8 @@ def create(self, validated_data): round = super().create(validated_data) - if len(motions_data) > 0: - for i, motion in enumerate(motions_data, start=1): - motion['seq'] = i - - motions = self.RoundMotionSerializer(many=True, context=self.context) - motions._validated_data = motions_data # Data was already validated - motions.save(round=round) + for i, motion in enumerate(motions_data, start=1): + save_related(self.RoundMotionSerializer, motion, self.context, {'round': round, 'seq': i}) return round @@ -359,10 +361,7 @@ def create(self, validated_data): rounds_data = validated_data.pop('roundmotion_set') motion = super().create(validated_data) - if len(rounds_data) > 0: - rounds = self.RoundsSerializer(many=True, context=self.context) - rounds._validated_data = rounds_data # Data was already validated - rounds.save(motion=motion) + save_related(self.RoundsSerializer, rounds_data, self.context, {'motion': motion}) return motion @@ -622,10 +621,7 @@ def create(self, validated_data): adj = super().create(validated_data) - if len(venue_constraints) > 0: - vc = VenueConstraintSerializer(many=True, context=self.context) - vc._validated_data = venue_constraints # Data was already validated - vc.save(subject=adj) + save_related(VenueConstraintSerializer, venue_constraints, self.context, {'subject': adj}) if url_key is None: # If explicitly null (and not just an empty string) populate_url_keys([adj]) @@ -639,11 +635,7 @@ def create(self, validated_data): return adj def update(self, instance, validated_data): - venue_constraints = validated_data.pop('venue_constraints', []) - if len(venue_constraints) > 0: - vc = VenueConstraintSerializer(many=True, context=self.context) - vc._validated_data = venue_constraints # Data was already validated - vc.save(subject=instance) + save_related(VenueConstraintSerializer, validated_data.pop('venue_constraints', []), self.context, {'subject': instance}) if 'base_score' in validated_data and validated_data['base_score'] != instance.base_score: AdjudicatorBaseScoreHistory.objects.create( @@ -768,15 +760,8 @@ def create(self, validated_data): ).exclude(pk__in=[bc.pk for bc in break_categories])) + break_categories) # The data is passed to the sub-serializer so that it handles categories - if len(speakers_data) > 0: - speakers = SpeakerSerializer(many=True, context=self.context) - speakers._validated_data = speakers_data # Data was already validated - speakers.save(team=team) - - if len(venue_constraints) > 0: - vc = VenueConstraintSerializer(many=True, context=self.context) - vc._validated_data = venue_constraints # Data was already validated - vc.save(subject=team) + save_related(SpeakerSerializer, speakers_data, self.context, {'team': team}) + save_related(VenueConstraintSerializer, venue_constraints, self.context, {'subject': team}) if team.institution is not None: team.teaminstitutionconflict_set.get_or_create(institution=team.institution) @@ -784,16 +769,8 @@ def create(self, validated_data): return team def update(self, instance, validated_data): - speakers_data = validated_data.pop('speakers', []) - venue_constraints = validated_data.pop('venue_constraints', []) - if len(speakers_data) > 0: - speakers = SpeakerSerializer(many=True, context=self.context) - speakers._validated_data = speakers_data # Data was already validated - speakers.save(team=instance) - if len(venue_constraints) > 0: - vc = VenueConstraintSerializer(many=True, context=self.context) - vc._validated_data = venue_constraints # Data was already validated - vc.save(subject=instance) + save_related(SpeakerSerializer, validated_data.pop('speakers', []), self.context, {'team': instance}) + save_related(VenueConstraintSerializer, validated_data.pop('venue_constraints', []), self.context, {'subject': instance}) if self.partial: # Avoid removing conflicts if merely PATCHing @@ -824,20 +801,12 @@ def create(self, validated_data): institution = super().create(validated_data) - if len(venue_constraints) > 0: - vc = VenueConstraintSerializer(many=True, context=self.context) - vc._validated_data = venue_constraints # Data was already validated - vc.save(subject=institution) + save_related(VenueConstraintSerializer, venue_constraints, self.context, {'subject': institution}) return institution def update(self, instance, validated_data): - venue_constraints = validated_data.pop('venue_constraints', []) - if len(venue_constraints) > 0: - vc = VenueConstraintSerializer(many=True, context=self.context) - vc._validated_data = venue_constraints # Data was already validated - vc.save(subject=instance) - + save_related(VenueConstraintSerializer, validated_data.pop('venue_constraints', []), self.context, {'subject': instance}) return super().update(instance, validated_data) @@ -970,7 +939,9 @@ class Meta: fields = ('team', 'side') def save(self, **kwargs): - kwargs['side'] = kwargs.get('side', kwargs['seq']) + seq = kwargs.pop('seq') + if 'side' not in self.validated_data: + self.validated_data['side'] = kwargs.get('side', seq) return super().save(**kwargs) class PairingLinksSerializer(serializers.Serializer): @@ -1007,15 +978,11 @@ def create(self, validated_data): validated_data['round'] = self.context['round'] debate = super().create(validated_data) - teams = self.DebateTeamSerializer() for i, team in enumerate(teams_data): - teams._validated_data = teams_data # Data was already validated - teams.save(debate=debate, seq=i) + save_related(self.DebateTeamSerializer, team, self.context, {'debate': debate, 'seq': i}) if adjs_data is not None: - adjudicators = DebateAdjudicatorSerializer() - adjudicators._validated_data = adjs_data - adjudicators.save(debate=debate) + save_related(DebateAdjudicatorSerializer, adjs_data, self.context, {'debate': debate}) return debate @@ -1028,10 +995,8 @@ def update(self, instance, validated_data): except (IntegrityError, TypeError) as e: raise serializers.ValidationError(e) - if 'adjudicators' in validated_data and validated_data['adjudicators'] is not None: - adjudicators = DebateAdjudicatorSerializer() - adjudicators._validated_data = validated_data.pop('adjudicators') - adjudicators.save(debate=instance) + if (adjs_data := validated_data.pop('adjudicators', None)) is not None: + save_related(DebateAdjudicatorSerializer, adjs_data, self.context, {'debate': instance}) return super().update(instance, validated_data) @@ -1300,15 +1265,13 @@ def save(self, **kwargs): args.insert(0, kwargs.get('adjudicator')) result.add_winner(*args) - speech_serializer = self.SpeechSerializer(context=self.context) for i, speech in enumerate(self.validated_data.get('speeches', []), 1): - speech_serializer._validated_data = speech - speech_serializer.save( - result=result, - side=side, - seq=i, - adjudicator=kwargs.get('adjudicator'), - ) + save_related(self.SpeechSerializer, speech, self.context, { + 'result': result, + 'side': side, + 'seq': i, + 'adjudicator': kwargs.get('adjudicator'), + }) return result teams = TeamResultSerializer(many=True) @@ -1335,10 +1298,12 @@ def validate_teams(self, value): return value def save(self, **kwargs): - team_serializer = self.TeamResultSerializer(context=self.context) for i, team in enumerate(self.validated_data.get('teams', [])): - team_serializer._validated_data = team - team_serializer.save(result=kwargs['result'], adjudicator=self.validated_data.get('adjudicator'), seq=i) + save_related(self.TeamResultSerializer, team, self.context, { + 'result': kwargs['result'], + 'adjudicator': self.validated_data.get('adjudicator'), + 'seq': i, + }) return kwargs['result'] sheets = SheetSerializer(many=True, required=True) @@ -1359,10 +1324,8 @@ def validate(self, data): def create(self, validated_data): result = DebateResult(validated_data['ballot'], tournament=self.context.get('tournament')) - sheets = self.SheetSerializer(context=self.context) for sheet in validated_data['sheets']: - sheets._validated_data = sheet - sheets.save(result=result) + save_related(self.SheetSerializer, sheet, self.context, {'result': result}) try: result.save() @@ -1455,16 +1418,10 @@ def create(self, validated_data): ballot = super().create(validated_data) - result = self.ResultSerializer(context=self.context) - result._validated_data = result_data - result._errors = [] - result.save(ballot=ballot) + save_related(self.ResultSerializer, result_data, self.context, {'ballot': ballot}) if veto_data: - vetos = self.VetoSerializer(context=self.context, many=True) - vetos._validated_data = veto_data - vetos._errors = [] - vetos.save(ballot_submission=ballot, preference=3) + save_related(self.VetoSerializer, veto_data, self.context, {'ballot_submission': ballot, 'preference': 3}) return ballot @@ -1501,17 +1458,13 @@ def create(self, validated_data): debate = super().create(validated_data) if adjs_data is not None: - adjudicators = DebateAdjudicatorSerializer() - adjudicators._validated_data = adjs_data - adjudicators.save(debate=debate) + save_related(DebateAdjudicatorSerializer, adjs_data, self.context, {'debate': debate}) return debate def update(self, instance, validated_data): if validated_data.get('adjudicators', None) is not None: - adjudicators = DebateAdjudicatorSerializer() - adjudicators._validated_data = validated_data.pop('adjudicators') - adjudicators.save(debate=instance) + save_related(DebateAdjudicatorSerializer, validated_data.pop('adjudicators'), self.context, {'debate': instance}) return super().update(instance, validated_data) From 0567df432720fcb1178fd64e0afee405bc51226c Mon Sep 17 00:00:00 2001 From: Dineth Date: Sun, 8 Dec 2024 10:45:19 +0530 Subject: [PATCH 2/3] Changed documentation (upgraded python version) --- docs/install/linux.rst | 10 +++++----- docs/install/osx.rst | 6 +++--- docs/install/windows.rst | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/install/linux.rst b/docs/install/linux.rst index f7a523ddda1..31c8ca6f644 100644 --- a/docs/install/linux.rst +++ b/docs/install/linux.rst @@ -28,7 +28,7 @@ Short version :: curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash - # add Node.js source repository - sudo apt install python3.9 python3-distutils pipenv postgresql libpq-dev nodejs gcc g++ make + sudo apt install python3.11 python3-distutils pipenv postgresql libpq-dev nodejs gcc g++ make git clone https://github.com/TabbycatDebate/tabbycat.git cd tabbycat git checkout master @@ -63,16 +63,16 @@ First, you need to install all of the software on which Tabbycat depends, if you 1(a). Python ------------ -Tabbycat uses Python 3.9. You probably already have Python 3, but you'll also need the development package in order to install Psycopg2 later. You'll also want `Pipenv `_, if you don't already have it. Install:: +Tabbycat uses Python 3.11. You probably already have Python 3, but you'll also need the development package in order to install Psycopg2 later. You'll also want `Pipenv `_, if you don't already have it. Install:: - $ sudo apt install python3.9 python3-distutils pipenv + $ sudo apt install python3.11 python3-distutils pipenv Check the version:: $ python3 --version - Python 3.9.12 + Python 3.11.10 -.. warning:: Tabbycat does not support Python 2. You must use Python 3.9. +.. warning:: Tabbycat does not support Python 2. You must use Python 3.11. .. admonition:: Advanced users :class: tip diff --git a/docs/install/osx.rst b/docs/install/osx.rst index 466e828b7ae..4cb727b50ab 100644 --- a/docs/install/osx.rst +++ b/docs/install/osx.rst @@ -30,14 +30,14 @@ First, you need to install all of the software on which Tabbycat depends, if you 1(a). Python -------------------------------------------------------------------------------- -Tabbycat requires Python 3.9. macOS only comes with Python 2.7, so you'll need to install this. You can download the latest version from the `Python website `_. +Tabbycat requires Python 3.11. macOS only comes with Python 2.7, so you'll need to install this. You can download the latest version from the `Python website `_. The executable will probably be called ``python3``, rather than ``python``. Check:: $ python3 --version - Python 3.9.12 + Python 3.11.10 -.. warning:: Tabbycat does not support Python 2. You must use Python 3.9. +.. warning:: Tabbycat does not support Python 2. You must use Python 3.11. You'll also need to install `Pipenv `_:: diff --git a/docs/install/windows.rst b/docs/install/windows.rst index aac98374866..2d7eb138724 100644 --- a/docs/install/windows.rst +++ b/docs/install/windows.rst @@ -63,7 +63,7 @@ To check that Python is installed correctly, open Windows PowerShell, type ``pyt .. note:: **If you already have Python**, great! Some things to double-check: - - You must have at least Python 3.9. + - You must have at least Python 3.11. - Your installation path must not have any spaces in it. - If that doesn't work, note that the following must be part of your ``PATH`` environment variable: ``C:\Python38;C:\Python38\Scripts`` (or as appropriate for your Python version and installation directory). Follow `the instructions here `_ to add this to your path. From 6067b42e29fcfa6fa7e5bed38a9df6b2564276b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Beaul=C3=A9?= Date: Mon, 9 Dec 2024 21:28:25 -0400 Subject: [PATCH 3/3] Add missing edit permissions for draw actions --- tabbycat/draw/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tabbycat/draw/views.py b/tabbycat/draw/views.py index 7cc6340ae54..2aacd391941 100644 --- a/tabbycat/draw/views.py +++ b/tabbycat/draw/views.py @@ -718,6 +718,7 @@ def post(self, request, *args, **kwargs): class ConfirmDrawCreationView(DrawStatusEdit): + edit_permission = Permission.GENERATE_DEBATE action_log_type = ActionLogEntry.ActionType.DRAW_CONFIRM def post(self, request, *args, **kwargs): @@ -737,6 +738,7 @@ def post(self, request, *args, **kwargs): class ConfirmDrawRegenerationView(LogActionMixin, AdministratorMixin, RoundMixin, FormView): template_name = "draw_confirm_regeneration.html" view_permission = Permission.DELETE_DEBATE + edit_permission = Permission.DELETE_DEBATE form_class = ConfirmDrawDeletionForm action_log_type = ActionLogEntry.ActionType.DRAW_REGENERATE