From ce526a4cc70609fd4e170d5ce0985a145a919b5d Mon Sep 17 00:00:00 2001 From: jackie / Andrea Ida Malkah Klaura Date: Thu, 18 Aug 2022 13:02:04 +0200 Subject: [PATCH 01/18] + add and use custom exception handler and redirect exception --- src/api/__init__.py | 46 +++++++++++++++++++++++++++++++++++++++ src/api/views.py | 4 +++- src/portfolio/settings.py | 1 + 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/api/__init__.py b/src/api/__init__.py index 23ca7a69..89ebe6e7 100644 --- a/src/api/__init__.py +++ b/src/api/__init__.py @@ -1 +1,47 @@ +import logging + +from rest_framework.exceptions import APIException +from rest_framework.views import exception_handler + default_app_config = 'api.apps.ApiConfig' + +logger = logging.getLogger(__name__) + + +class PermanentRedirect(APIException): + # TODO: in the current version rest_framework.status has a status.HTTP_307_TEMPORARY_REDIRECT, but it is missing a + # status.HTTP_308_PERMANENT_REDIRECT, which is available in a newer version of rest_framework. Update this, + # as soon as rest_framework is updated. + # status_code = status.HTTP_308_PERMANENT_REDIRECT + status_code = 308 + default_detail = 'This resource has moved' + default_code = 'permanent_redirect' + + def __init__(self, detail=None, to=None): + self.to = to + if detail is None: + detail = self.default_detail + super().__init__(detail) + + +def portfolio_exception_handler(exc, context): + # Call REST framework's default exception handler first, + # to get the standard error response. + response = exception_handler(exc, context) + + if isinstance(exc, PermanentRedirect): + pk = context['kwargs']['pk'] + old_path = context['request']._request.path + if exc.to: + new_path = old_path.replace(pk, exc.to) + response.data['to'] = new_path + # TODO: check why headers is not here? issue with rest_framework version? + # response.headers['Location'] = new_path + else: + # TODO: in case to was not set when the exception was raised, should we + # rather convert this to a 404, or should we even raise another + # exception and go for a 500? + response.data['to'] = 'location not disclosed' + logger.warning(f'PermanentRedirect: no to parameter was provided for {old_path}') + + return response diff --git a/src/api/views.py b/src/api/views.py index 028a8169..9db2004d 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -40,6 +40,7 @@ from media_server.models import get_media_for_entry, update_media_order_for_entry from media_server.utils import get_free_space_for_user +from . import PermanentRedirect from .serializers.entry import EntrySerializer from .serializers.relation import RelationSerializer from .yasg import ( @@ -174,7 +175,8 @@ def retrieve(self, request, *args, **kwargs): except Http404 as nfe: reverse_pk = kwargs.get('pk', '')[::-1] if self.get_queryset().filter(pk=reverse_pk).exists(): - return Response(reverse_pk, status=301) + raise PermanentRedirect(to=reverse_pk) from nfe + # return Response(reverse_pk, status=301) else: raise nfe serializer = self.get_serializer(instance) diff --git a/src/portfolio/settings.py b/src/portfolio/settings.py index a2fe4d9c..04fe725d 100644 --- a/src/portfolio/settings.py +++ b/src/portfolio/settings.py @@ -379,6 +379,7 @@ 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning', 'DEFAULT_VERSION': 'v1', 'ORDERING_PARAM': 'sort', + 'EXCEPTION_HANDLER': 'api.portfolio_exception_handler', } SWAGGER_SETTINGS = {'SECURITY_DEFINITIONS': {}} From 3c837b3b2ebf0031bd05c4dc92fdac9ab73f1168 Mon Sep 17 00:00:00 2001 From: jackie / Andrea Ida Malkah Klaura Date: Wed, 9 Nov 2022 14:44:20 +0100 Subject: [PATCH 02/18] fix(api): use backwards-compatible header location --- src/api/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/__init__.py b/src/api/__init__.py index 89ebe6e7..310df613 100644 --- a/src/api/__init__.py +++ b/src/api/__init__.py @@ -35,8 +35,8 @@ def portfolio_exception_handler(exc, context): if exc.to: new_path = old_path.replace(pk, exc.to) response.data['to'] = new_path - # TODO: check why headers is not here? issue with rest_framework version? - # response.headers['Location'] = new_path + # TODO: update to response.headers['Location'] once django is updated to >= 3.2 + response['Location'] = new_path else: # TODO: in case to was not set when the exception was raised, should we # rather convert this to a 404, or should we even raise another From 24975a0d4d0b97ed8074ca47d401052832b4e71f Mon Sep 17 00:00:00 2001 From: jackie / Andrea Ida Malkah Klaura Date: Mon, 21 Nov 2022 15:56:07 +0100 Subject: [PATCH 03/18] fix: use empty dict as default for Entry.data While Portfolio Frontend and Showroom Backend already applied fixes to handle missing `data` properties in Portfolio entries, this now makes sure that also Portfolio backend validates all entries to have at least an empty dict instead of a potentially missing (None/null) value. Refs: #2150 --- .../migrations/0019_auto_20221121_1114.py | 19 +++++++++++++++++++ src/core/models.py | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 src/core/migrations/0019_auto_20221121_1114.py diff --git a/src/core/migrations/0019_auto_20221121_1114.py b/src/core/migrations/0019_auto_20221121_1114.py new file mode 100644 index 00000000..165b637c --- /dev/null +++ b/src/core/migrations/0019_auto_20221121_1114.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.28 on 2022-11-21 10:14 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0018_auto_20220422_0809'), + ] + + operations = [ + migrations.AlterField( + model_name='entry', + name='data', + field=django.contrib.postgres.fields.jsonb.JSONField(default=dict), + ), + ] diff --git a/src/core/models.py b/src/core/models.py index a59f1f69..e0a14214 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -32,7 +32,7 @@ class Entry(AbstractBaseModel): ) texts = JSONField(verbose_name=get_preflabel_lazy('text'), validators=[validate_texts], blank=True, null=True) published = models.BooleanField(default=False) - data = JSONField(blank=True, null=True) + data = JSONField(default=dict) relations = models.ManyToManyField('self', through='Relation', symmetrical=False, related_name='related_to') reference = models.CharField(max_length=255, blank=True, null=True, default=None) From cc35c566c669b33de13ac9daee788316e756edbd Mon Sep 17 00:00:00 2001 From: Philipp Mayer Date: Fri, 20 Jan 2023 17:05:16 +0100 Subject: [PATCH 04/18] chore: update pre-commit config --- .pre-commit-config.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6439e67e..bcf970e3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,8 +1,10 @@ +default_install_hook_types: [pre-commit, commit-msg] default_language_version: python: python3 exclude: (/migrations/|manage.py|docs/source/conf.py) repos: - repo: https://github.com/base-angewandte/pre-commit-hooks - rev: py3.7 + rev: 1.1.1-py3.7 hooks: - id: base-hooks + - id: base-commit-msg-hooks From 88a51be4f44dff81862f0653bef896a868a1884f Mon Sep 17 00:00:00 2001 From: Philipp Mayer Date: Fri, 20 Jan 2023 17:46:27 +0100 Subject: [PATCH 05/18] fix: fix hadolint DL4006 --- src/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Dockerfile b/src/Dockerfile index 42d5b783..ab745843 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -18,6 +18,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ webp \ && rm -rf /var/lib/apt/lists/* +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + # hadolint ignore=DL3003 RUN EXIFTOOL_VERSION="$(wget -qO- https://api.github.com/repos/exiftool/exiftool/tags | jq -r '.[0].name')" \ && wget --progress=dot:giga https://netix.dl.sourceforge.net/project/exiftool/Image-ExifTool-"${EXIFTOOL_VERSION}".tar.gz \ From c8d95a8332fa2759b939a437b8cb983cf363006c Mon Sep 17 00:00:00 2001 From: Philipp Mayer Date: Fri, 20 Jan 2023 17:46:46 +0100 Subject: [PATCH 06/18] fix: exchange sourceforge with github for installing exiftool --- src/Dockerfile | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Dockerfile b/src/Dockerfile index ab745843..16557ce7 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -22,14 +22,15 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"] # hadolint ignore=DL3003 RUN EXIFTOOL_VERSION="$(wget -qO- https://api.github.com/repos/exiftool/exiftool/tags | jq -r '.[0].name')" \ - && wget --progress=dot:giga https://netix.dl.sourceforge.net/project/exiftool/Image-ExifTool-"${EXIFTOOL_VERSION}".tar.gz \ - && tar xvf Image-ExifTool-"${EXIFTOOL_VERSION}".tar.gz \ - && cd Image-ExifTool-"${EXIFTOOL_VERSION}"/ \ + EXIFTOOL_FILENAME=exiftool-"${EXIFTOOL_VERSION}" \ + && wget --progress=dot:giga https://github.com/exiftool/exiftool/archive/refs/tags/"${EXIFTOOL_VERSION}".tar.gz -O "${EXIFTOOL_FILENAME}".tar.gz \ + && tar xvf "${EXIFTOOL_FILENAME}".tar.gz \ + && cd "${EXIFTOOL_FILENAME}"/ \ && perl Makefile.PL \ && make test \ - && make install \ - && cd .. \ - && rm -rf Image-ExifTool-"${EXIFTOOL_VERSION}"* + && make install \ + && cd .. \ + && rm -rf "${EXIFTOOL_FILENAME}"* # hadolint ignore=DL3059 RUN mkdir /logs From 67cdc66d76dda6331309281394c3687c13965195 Mon Sep 17 00:00:00 2001 From: Philipp Mayer Date: Mon, 23 Jan 2023 10:07:35 +0100 Subject: [PATCH 07/18] feat(api): add all parameter to /api/v1/user/{id}/data/ --- src/api/views.py | 71 +++++++++++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/src/api/views.py b/src/api/views.py index 55b65b3d..3f6246c9 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -340,7 +340,17 @@ def user_information(request, *args, **kwargs): 403: openapi.Response('Access not allowed'), 404: openapi.Response('User not found'), }, - manual_parameters=[authorization_header_paramter, language_header_parameter], + manual_parameters=[ + authorization_header_paramter, + language_header_parameter, + openapi.Parameter( + 'all', + openapi.IN_QUERY, + required=False, + type=openapi.TYPE_BOOLEAN, + default=False, + ), + ], ) @api_view(['GET']) @authentication_classes((TokenAuthentication,)) @@ -374,34 +384,39 @@ def to_data_dict(label, data, sort=True): 'data': data, } - published_entries_query = Entry.objects.filter(owner=user, published=True, type__isnull=False,).filter( - Q(data__contains={'architecture': [{'source': user.username}]}) - | Q(data__contains={'authors': [{'source': user.username}]}) - | Q(data__contains={'artists': [{'source': user.username}]}) - | Q(data__contains={'winners': [{'source': user.username}]}) - | Q(data__contains={'granted_by': [{'source': user.username}]}) - | Q(data__contains={'jury': [{'source': user.username}]}) - | Q(data__contains={'music': [{'source': user.username}]}) - | Q(data__contains={'conductors': [{'source': user.username}]}) - | Q(data__contains={'composition': [{'source': user.username}]}) - | Q(data__contains={'organisers': [{'source': user.username}]}) - | Q(data__contains={'lecturers': [{'source': user.username}]}) - | Q(data__contains={'design': [{'source': user.username}]}) - | Q(data__contains={'commissions': [{'source': user.username}]}) - | Q(data__contains={'editors': [{'source': user.username}]}) - | Q(data__contains={'publishers': [{'source': user.username}]}) - | Q(data__contains={'curators': [{'source': user.username}]}) - | Q(data__contains={'fellow_scholar': [{'source': user.username}]}) - | Q(data__contains={'funding': [{'source': user.username}]}) - | Q(data__contains={'organisations': [{'source': user.username}]}) - | Q(data__contains={'project_lead': [{'source': user.username}]}) - | Q(data__contains={'project_partnership': [{'source': user.username}]}) - | Q(data__contains={'software_developers': [{'source': user.username}]}) - | Q(data__contains={'directors': [{'source': user.username}]}) - | Q(data__contains={'contributors': [{'source': user.username}]}) - ) + all_parameter = request.query_params.get('all', False) + + published_entries_query = Entry.objects.filter(owner=user, published=True, type__isnull=False) + + if not all_parameter: + published_entries_query = published_entries_query.filter( + Q(data__contains={'architecture': [{'source': user.username}]}) + | Q(data__contains={'authors': [{'source': user.username}]}) + | Q(data__contains={'artists': [{'source': user.username}]}) + | Q(data__contains={'winners': [{'source': user.username}]}) + | Q(data__contains={'granted_by': [{'source': user.username}]}) + | Q(data__contains={'jury': [{'source': user.username}]}) + | Q(data__contains={'music': [{'source': user.username}]}) + | Q(data__contains={'conductors': [{'source': user.username}]}) + | Q(data__contains={'composition': [{'source': user.username}]}) + | Q(data__contains={'organisers': [{'source': user.username}]}) + | Q(data__contains={'lecturers': [{'source': user.username}]}) + | Q(data__contains={'design': [{'source': user.username}]}) + | Q(data__contains={'commissions': [{'source': user.username}]}) + | Q(data__contains={'editors': [{'source': user.username}]}) + | Q(data__contains={'publishers': [{'source': user.username}]}) + | Q(data__contains={'curators': [{'source': user.username}]}) + | Q(data__contains={'fellow_scholar': [{'source': user.username}]}) + | Q(data__contains={'funding': [{'source': user.username}]}) + | Q(data__contains={'organisations': [{'source': user.username}]}) + | Q(data__contains={'project_lead': [{'source': user.username}]}) + | Q(data__contains={'project_partnership': [{'source': user.username}]}) + | Q(data__contains={'software_developers': [{'source': user.username}]}) + | Q(data__contains={'directors': [{'source': user.username}]}) + | Q(data__contains={'contributors': [{'source': user.username}]}) + ) - cache_key = f'user_data__{pk}_{lang}' + cache_key = f'user_data__{pk}_{lang}_{all_parameter}' cache_time, entries_count, usr_data = cache.get(cache_key, (None, None, None)) From 827e645c16f524b4864aad99b548c8cb055d9123 Mon Sep 17 00:00:00 2001 From: Philipp Mayer Date: Mon, 23 Jan 2023 10:48:44 +0100 Subject: [PATCH 08/18] refactor: move duplicate code to separate function --- src/api/views.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/api/views.py b/src/api/views.py index 3f6246c9..5c093484 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -1036,6 +1036,16 @@ def get_media_for_entry_public(entry): return media +def get_entry_data(entry): + ret = entry.data_display + ret['media'] = get_media_for_entry_public(entry.pk) + ret['relations'] = { + 'parents': [{'id': r.pk, 'title': r.title} for r in entry.related_to.filter(published=True)], + 'to': [{'id': r.pk, 'title': r.title} for r in entry.relations.filter(published=True)], + } + return ret + + @swagger_auto_schema( methods=['get'], operation_id='api_v1_user_entry_data', @@ -1062,13 +1072,7 @@ def user_entry_data(request, pk=None, entry=None, *args, **kwargs): except Entry.DoesNotExist as e: raise exceptions.NotFound(_('Entry does not exist')) from e - ret = e.data_display - ret['media'] = get_media_for_entry_public(entry) - ret['relations'] = { - 'parents': [{'id': r.pk, 'title': r.title} for r in e.related_to.filter(published=True)], - 'to': [{'id': r.pk, 'title': r.title} for r in e.relations.filter(published=True)], - } - + ret = get_entry_data(e) return Response(ret) @@ -1091,13 +1095,7 @@ def entry_data(request, pk=None, *args, **kwargs): except Entry.DoesNotExist as e: raise exceptions.NotFound(_('Entry does not exist')) from e - ret = e.data_display - ret['media'] = get_media_for_entry_public(pk) - ret['relations'] = { - 'parents': [{'id': r.pk, 'title': r.title} for r in e.related_to.filter(published=True)], - 'to': [{'id': r.pk, 'title': r.title} for r in e.relations.filter(published=True)], - } - + ret = get_entry_data(e) return Response(ret) From c8ff76965b6610b50e95f51aab1976514df7ce01 Mon Sep 17 00:00:00 2001 From: Philipp Mayer Date: Mon, 23 Jan 2023 10:49:29 +0100 Subject: [PATCH 09/18] feat(api): add showroom_id to entry data responses --- src/api/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/views.py b/src/api/views.py index 5c093484..d0eb2cb2 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -1043,6 +1043,7 @@ def get_entry_data(entry): 'parents': [{'id': r.pk, 'title': r.title} for r in entry.related_to.filter(published=True)], 'to': [{'id': r.pk, 'title': r.title} for r in entry.relations.filter(published=True)], } + ret['showroom_id'] = entry.showroom_id return ret From d1095282150677704720c7cb6b1f066d60e52351 Mon Sep 17 00:00:00 2001 From: Philipp Mayer Date: Tue, 24 Jan 2023 13:34:15 +0100 Subject: [PATCH 10/18] fix(api): parse boolean correctly and raise 400 in case of malformed parameter --- src/api/views.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/api/views.py b/src/api/views.py index d0eb2cb2..ce8c7ef3 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -1,3 +1,4 @@ +import json import operator from functools import reduce @@ -337,6 +338,7 @@ def user_information(request, *args, **kwargs): operation_id='api_v1_user_data', responses={ 200: openapi.Response(''), + 400: openapi.Response('Bad Request'), 403: openapi.Response('Access not allowed'), 404: openapi.Response('User not found'), }, @@ -384,7 +386,10 @@ def to_data_dict(label, data, sort=True): 'data': data, } - all_parameter = request.query_params.get('all', False) + try: + all_parameter = json.loads(request.query_params.get('all', 'false')) + except json.JSONDecodeError as e: + raise exceptions.ParseError() from e published_entries_query = Entry.objects.filter(owner=user, published=True, type__isnull=False) From c40cda2de9aca1af0a799f4d72a76fcabcf8761f Mon Sep 17 00:00:00 2001 From: Philipp Mayer Date: Tue, 24 Jan 2023 14:13:57 +0100 Subject: [PATCH 11/18] fix(api): handle empty type --- src/api/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/views.py b/src/api/views.py index ce8c7ef3..fc6117f5 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -372,7 +372,7 @@ def entry_to_data(entry): 'id': entry.pk, 'title': entry.title, 'subtitle': entry.subtitle or None, - 'type': entry.type.get('label').get(lang), + 'type': entry.type.get('label').get(lang) if entry.type else None, 'role': entry.owner_role_display, 'location': entry.location_display, 'year': entry.year_display, @@ -941,7 +941,7 @@ def to_data_dict(label, data, sort=True): videos_data.append(entry_to_data(e)) # General Activites else: - general_activities_data.append(e) + general_activities_data.append(entry_to_data(e)) # Publications publications_data = [] From 6206fa103a0aaabbc35e3d19280750cd4a9f2164 Mon Sep 17 00:00:00 2001 From: Philipp Mayer Date: Tue, 24 Jan 2023 14:19:51 +0100 Subject: [PATCH 12/18] change: better exclusion of empty type --- src/api/views.py | 65 ++++++++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/src/api/views.py b/src/api/views.py index fc6117f5..0281b026 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -236,7 +236,14 @@ def media_order(self, request, pk=None, *args, **kwargs): @action(detail=False, filter_backends=[], pagination_class=None) def types(self, request, *args, **kwargs): language = get_language() or 'en' - content = self.get_queryset().exclude(type__isnull=True).values_list('type', flat=True).distinct().order_by() + content = ( + self.get_queryset() + .exclude(type__isnull=True) + .exclude(type={}) + .values_list('type', flat=True) + .distinct() + .order_by() + ) return Response(sorted(content, key=lambda x: x.get('label', {}).get(language, '').lower())) def get_queryset(self): @@ -391,34 +398,38 @@ def to_data_dict(label, data, sort=True): except json.JSONDecodeError as e: raise exceptions.ParseError() from e - published_entries_query = Entry.objects.filter(owner=user, published=True, type__isnull=False) + published_entries_query = Entry.objects.filter(owner=user, published=True) if not all_parameter: - published_entries_query = published_entries_query.filter( - Q(data__contains={'architecture': [{'source': user.username}]}) - | Q(data__contains={'authors': [{'source': user.username}]}) - | Q(data__contains={'artists': [{'source': user.username}]}) - | Q(data__contains={'winners': [{'source': user.username}]}) - | Q(data__contains={'granted_by': [{'source': user.username}]}) - | Q(data__contains={'jury': [{'source': user.username}]}) - | Q(data__contains={'music': [{'source': user.username}]}) - | Q(data__contains={'conductors': [{'source': user.username}]}) - | Q(data__contains={'composition': [{'source': user.username}]}) - | Q(data__contains={'organisers': [{'source': user.username}]}) - | Q(data__contains={'lecturers': [{'source': user.username}]}) - | Q(data__contains={'design': [{'source': user.username}]}) - | Q(data__contains={'commissions': [{'source': user.username}]}) - | Q(data__contains={'editors': [{'source': user.username}]}) - | Q(data__contains={'publishers': [{'source': user.username}]}) - | Q(data__contains={'curators': [{'source': user.username}]}) - | Q(data__contains={'fellow_scholar': [{'source': user.username}]}) - | Q(data__contains={'funding': [{'source': user.username}]}) - | Q(data__contains={'organisations': [{'source': user.username}]}) - | Q(data__contains={'project_lead': [{'source': user.username}]}) - | Q(data__contains={'project_partnership': [{'source': user.username}]}) - | Q(data__contains={'software_developers': [{'source': user.username}]}) - | Q(data__contains={'directors': [{'source': user.username}]}) - | Q(data__contains={'contributors': [{'source': user.username}]}) + published_entries_query = ( + published_entries_query.exclude(type__isnull=True) + .exclude(type={}) + .filter( + Q(data__contains={'architecture': [{'source': user.username}]}) + | Q(data__contains={'authors': [{'source': user.username}]}) + | Q(data__contains={'artists': [{'source': user.username}]}) + | Q(data__contains={'winners': [{'source': user.username}]}) + | Q(data__contains={'granted_by': [{'source': user.username}]}) + | Q(data__contains={'jury': [{'source': user.username}]}) + | Q(data__contains={'music': [{'source': user.username}]}) + | Q(data__contains={'conductors': [{'source': user.username}]}) + | Q(data__contains={'composition': [{'source': user.username}]}) + | Q(data__contains={'organisers': [{'source': user.username}]}) + | Q(data__contains={'lecturers': [{'source': user.username}]}) + | Q(data__contains={'design': [{'source': user.username}]}) + | Q(data__contains={'commissions': [{'source': user.username}]}) + | Q(data__contains={'editors': [{'source': user.username}]}) + | Q(data__contains={'publishers': [{'source': user.username}]}) + | Q(data__contains={'curators': [{'source': user.username}]}) + | Q(data__contains={'fellow_scholar': [{'source': user.username}]}) + | Q(data__contains={'funding': [{'source': user.username}]}) + | Q(data__contains={'organisations': [{'source': user.username}]}) + | Q(data__contains={'project_lead': [{'source': user.username}]}) + | Q(data__contains={'project_partnership': [{'source': user.username}]}) + | Q(data__contains={'software_developers': [{'source': user.username}]}) + | Q(data__contains={'directors': [{'source': user.username}]}) + | Q(data__contains={'contributors': [{'source': user.username}]}) + ) ) cache_key = f'user_data__{pk}_{lang}_{all_parameter}' From d1b32c2267ec91cdbbe4a66e21562c57a11b9eb9 Mon Sep 17 00:00:00 2001 From: Philipp Mayer Date: Tue, 24 Jan 2023 14:32:31 +0100 Subject: [PATCH 13/18] fix(api): handle empty type --- src/api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/views.py b/src/api/views.py index 0281b026..f7bcf027 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -605,7 +605,7 @@ def to_data_dict(label, data, sort=True): published_entries = published_entries_query.order_by('title') for e in published_entries: - entry_type = e.type.get('source') + entry_type = e.type.get('source') if e.type else None if entry_type in DOCUMENT_TYPES: e_data = document_schema.load(e.data).data From 2e380cef90d8780560c1dd27c0e319d780c4040d Mon Sep 17 00:00:00 2001 From: Philipp Mayer Date: Tue, 24 Jan 2023 14:40:01 +0100 Subject: [PATCH 14/18] fix(api): handle empty type --- src/core/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/models.py b/src/core/models.py index e0a14214..c791ce3d 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -54,7 +54,7 @@ def icon(self): @property def location_display(self): - if self.type.get('source'): + if self.type and self.type.get('source'): schema = get_schema(self.type['source']) data = self.data if schema and data: @@ -62,7 +62,7 @@ def location_display(self): @property def owner_role_display(self): - if self.type.get('source'): + if self.type and self.type.get('source'): schema = get_schema(self.type['source']) data = self.data if schema and data: @@ -70,7 +70,7 @@ def owner_role_display(self): @property def year_display(self): - if self.type.get('source'): + if self.type and self.type.get('source'): schema = get_schema(self.type['source']) data = self.data if schema and data: From 7ac745eb963297e710e1e7fd96c3dd518069f85f Mon Sep 17 00:00:00 2001 From: Philipp Mayer Date: Wed, 25 Jan 2023 16:26:42 +0100 Subject: [PATCH 15/18] chore: cleanup commented code --- src/api/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/api/views.py b/src/api/views.py index a2a1ef7e..f456ccc3 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -166,7 +166,6 @@ def retrieve(self, request, *args, **kwargs): reverse_pk = kwargs.get('pk', '')[::-1] if self.get_queryset().filter(pk=reverse_pk).exists(): raise PermanentRedirect(to=reverse_pk) from nfe - # return Response(reverse_pk, status=301) else: raise nfe serializer = self.get_serializer(instance) From 1928b7f7bddab749afc9b23c4dee62d315c70e02 Mon Sep 17 00:00:00 2001 From: Philipp Mayer Date: Thu, 26 Jan 2023 09:58:21 +0100 Subject: [PATCH 16/18] change: make 'to' argument required --- src/api/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/api/__init__.py b/src/api/__init__.py index 310df613..2e7a81e1 100644 --- a/src/api/__init__.py +++ b/src/api/__init__.py @@ -18,6 +18,8 @@ class PermanentRedirect(APIException): default_code = 'permanent_redirect' def __init__(self, detail=None, to=None): + if to is None: + raise TypeError("PermanentRedirect is missing required argument 'to'") self.to = to if detail is None: detail = self.default_detail From 5276297793f041155acbdd3d20c9cf75b4efff20 Mon Sep 17 00:00:00 2001 From: Philipp Mayer Date: Thu, 26 Jan 2023 10:00:15 +0100 Subject: [PATCH 17/18] remove: remove unnecessary if/else --- src/api/__init__.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/api/__init__.py b/src/api/__init__.py index 2e7a81e1..9d634108 100644 --- a/src/api/__init__.py +++ b/src/api/__init__.py @@ -34,16 +34,9 @@ def portfolio_exception_handler(exc, context): if isinstance(exc, PermanentRedirect): pk = context['kwargs']['pk'] old_path = context['request']._request.path - if exc.to: - new_path = old_path.replace(pk, exc.to) - response.data['to'] = new_path - # TODO: update to response.headers['Location'] once django is updated to >= 3.2 - response['Location'] = new_path - else: - # TODO: in case to was not set when the exception was raised, should we - # rather convert this to a 404, or should we even raise another - # exception and go for a 500? - response.data['to'] = 'location not disclosed' - logger.warning(f'PermanentRedirect: no to parameter was provided for {old_path}') + new_path = old_path.replace(pk, exc.to) + response.data['to'] = new_path + # TODO: update to response.headers['Location'] once django is updated to >= 3.2 + response['Location'] = new_path return response From 1f4caa93b55608e961e5c05850d81cea8bef2ffe Mon Sep 17 00:00:00 2001 From: Philipp Mayer Date: Thu, 26 Jan 2023 14:54:25 +0100 Subject: [PATCH 18/18] docs: update CHANGELOG for 1.3.1 --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6f2acf9..fe86d504 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 1.3.1 + +## Added + +- Added `all` parameter to `/api/v1/user/{id}/data/` to be able to also return entries in which the user isn't a contributor + +### Changed + +- **BREAKING**: Updated pre-commit configuration to also enforce the use of conventional commit messages +- **BREAKING**: Changed redirect response from 301 to 308 +- **BREAKING**: Default value for `data` is now an empty dict +- Install exiftool via github instead of sourceforge in docker image + ## 1.3 ### Added