From 7c29f2496b6cd83a8296961cfb31a28d4b8d6a07 Mon Sep 17 00:00:00 2001 From: pelmat <58703747+pelmat@users.noreply.github.com> Date: Wed, 3 Nov 2021 09:22:54 +0200 Subject: [PATCH] Feature/offer payment methods (#55) * Preliminary changes to payment methods Preliminary changes to payment methods, includes model changes, multilanguage translation changes, admin view changes and a new api endpoint. * Handle payment_method linking * Fixed validation for Image publication rights. This bug caused us problems with all new models requiring editable rights. This has now been resolved. * Validate that the models have is_user_editable attribute We also need to validate that the models have the is_user_editable function. * Importer for 'Payment Methods' default values Importer for Payment Methods that adds the default values into the database. * Checks for the existence of both attributes. Checks for the existence of both attributes. * PMD update PMD update * Updated PMD index start value Updated PMD index start value * Update api.py New offer update, changes to OfferSerializer * Create 0083_auto_20211102_1005.py New migration file for payment methods * Update models.py Removed unused import Co-authored-by: ezkat <50319957+ezkat@users.noreply.github.com> Co-authored-by: Anthon98 --- events/admin.py | 28 ++++++-- events/api.py | 52 +++++++++++--- events/importer/payment_method_defaults.py | 71 ++++++++++++++++++++ events/migrations/0083_auto_20211102_1005.py | 33 +++++++++ events/models.py | 10 +++ events/translation.py | 13 +++- 6 files changed, 190 insertions(+), 17 deletions(-) create mode 100644 events/importer/payment_method_defaults.py create mode 100644 events/migrations/0083_auto_20211102_1005.py diff --git a/events/admin.py b/events/admin.py index 77fecab1b..847fcea05 100644 --- a/events/admin.py +++ b/events/admin.py @@ -6,7 +6,7 @@ from reversion.admin import VersionAdmin from admin_auto_filters.filters import AutocompleteFilter from events.api import generate_id -from events.models import Place, License, DataSource, Event, Keyword, KeywordSet, Language +from events.models import Place, License, DataSource, Event, Keyword, KeywordSet, Language, PaymentMethod class BaseAdmin(admin.ModelAdmin): @@ -57,11 +57,14 @@ class EventAdmin(AutoIdBaseAdmin, TranslationAdmin, VersionAdmin): 'provider_contact_info', 'event_status', 'super_event', 'info_url', 'in_language', 'publication_status', 'replaced_by', 'deleted') search_fields = ('name', 'location__name') - list_display = ('id', 'name', 'start_time', 'end_time', 'publisher', 'location') - list_filter = ('data_source', PublisherFilter, CreatedByFilter, LocationFilter) + list_display = ('id', 'name', 'start_time', + 'end_time', 'publisher', 'location') + list_filter = ('data_source', PublisherFilter, + CreatedByFilter, LocationFilter) ordering = ('-last_modified_time',) date_hierarchy = 'end_time' - autocomplete_fields = ('location', 'keywords', 'audience', 'super_event', 'publisher', 'replaced_by') + autocomplete_fields = ('location', 'keywords', 'audience', + 'super_event', 'publisher', 'replaced_by') def get_readonly_fields(self, request, obj=None): if obj: @@ -78,7 +81,8 @@ class Media: class KeywordAdmin(AutoIdBaseAdmin, TranslationAdmin, VersionAdmin): # TODO: only allow user_editable editable fields - fields = ('id', 'data_source', 'origin_id', 'publisher', 'name', 'replaced_by', 'deprecated') + fields = ('id', 'data_source', 'origin_id', 'publisher', + 'name', 'replaced_by', 'deprecated') search_fields = ('name',) list_display = ('id', 'name', 'n_events') list_filter = ('data_source',) @@ -194,3 +198,17 @@ def get_readonly_fields(self, request, obj=None): admin.site.register(License, LicenseAdmin) + + +class PaymentAdmin(BaseAdmin, TranslationAdmin): + fields = ('id', 'name') + list_display = ('id', 'name') + + def get_readonly_fields(self, request, obj=None): + if obj: + return ['id'] + else: + return [] + + +admin.site.register(PaymentMethod, PaymentAdmin) diff --git a/events/api.py b/events/api.py index be138c39c..4af50011d 100644 --- a/events/api.py +++ b/events/api.py @@ -63,7 +63,7 @@ from events.extensions import apply_select_and_prefetch, get_extensions_from_request from events.models import ( Place, Event, Keyword, KeywordSet, Language, OpeningHoursSpecification, EventLink, - Offer, DataSource, Image, PublicationStatus, PUBLICATION_STATUSES, License, Video + Offer, DataSource, Image, PublicationStatus, PUBLICATION_STATUSES, License, Video, PaymentMethod ) from events.translation import EventTranslationOptions, ImageTranslationOptions from helevents.models import User @@ -260,6 +260,10 @@ def to_internal_value(self, value): self.invalid_json_error % type(value).__name__) url = value['@id'] + + if not isinstance(url, str): + url = str(url) + if not url: if self.required: raise serializers.ValidationError(_('This field is required.')) @@ -513,12 +517,13 @@ def __init__(self, instance=None, files=None, if not instance.data_source == self.data_source: raise PermissionDenied() else: - # without api key, the user will have to be admin - if not instance.is_user_editable() or not instance.can_be_edited_by(self.user): - # An exception to allow users to publish events using default images from the Imagebank - # even if they aren't bound to the Imagebank-organization for regular or admin rights. - if not isinstance(instance, Image): - raise PermissionDenied() + if not isinstance(instance, Image): + ''' Without the API key, the user needs Admin rights. + An exception to allow users to publish events using default images from the Imagebank + even if they aren't bound to the Imagebank-organization for regular or admin rights. ''' + if hasattr(instance, 'is_user_editable') and hasattr(instance, 'can_be_edited_by'): + if not instance.is_user_editable() or not instance.can_be_edited_by(self.user): + raise PermissionDenied() def to_internal_value(self, data): for field in self.system_generated_fields: @@ -1134,7 +1139,29 @@ class Meta: exclude = ['id', 'event'] +class PaymentMethodSerializer(LinkedEventsSerializer): + view_name = 'paymentmethod-detail' + id = serializers.CharField(required=False) + name = serializers.CharField(required=False) + + class Meta: + model = PaymentMethod + fields = '__all__' + + +class PaymentViewSet(JSONAPIViewMixin, viewsets.ReadOnlyModelViewSet): + queryset = PaymentMethod.objects.all() + serializer_class = PaymentMethodSerializer + + +register_view(PaymentViewSet, 'paymentmethod') + + class OfferSerializer(TranslatedModelSerializer): + payment_methods = JSONLDRelatedField( + serializer=PaymentMethodSerializer, many=True, required=False, allow_empty=True, expanded=True, + view_name='paymentmethod-detail', queryset=PaymentMethod.objects.all()) + class Meta: model = Offer exclude = ['id', 'event'] @@ -1467,9 +1494,11 @@ def create(self, validated_data): event = super().create(validated_data) - # create and add related objects for offer in offers: - Offer.objects.create(event=event, **offer) + payment_methods = offer.pop('payment_methods', []) + offer_instance = Offer.objects.create(event=event, **offer) + for payment_method in payment_methods: + offer_instance.payment_methods.add(payment_method) for link in links: EventLink.objects.create(event=event, **link) for video in videos: @@ -1539,7 +1568,10 @@ def update(self, instance, validated_data): if isinstance(offers, list): instance.offers.all().delete() for offer in offers: - Offer.objects.create(event=instance, **offer) + payment_methods = offer.pop('payment_methods', []) + offer_instance = Offer.objects.create(event=instance, **offer) + for payment_method in payment_methods: + offer_instance.payment_methods.add(payment_method) # update ext links if isinstance(links, list): diff --git a/events/importer/payment_method_defaults.py b/events/importer/payment_method_defaults.py new file mode 100644 index 000000000..be5535156 --- /dev/null +++ b/events/importer/payment_method_defaults.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +# Dependencies. + +# Logging: +import time +import logging +from os import mkdir +from os.path import abspath, join, dirname, exists, basename, splitext + +# Django: +from django_orghierarchy.models import Organization +from django_orghierarchy.models import OrganizationClass +from events.models import BaseModel, PaymentMethod + +# Importer specific: +from .base import Importer, register_importer + +# Type checking: +from typing import Any + +# Setup Logging: +if not exists(join(dirname(__file__), 'logs')): + mkdir(join(dirname(__file__), 'logs')) + +logger = logging.getLogger(__name__) # Per module logger +curFileExt = basename(__file__) +curFile = splitext(curFileExt)[0] +logFile = \ + logging.FileHandler( + '%s' % (join(dirname(__file__), 'logs', curFile+'.logs')) + ) +logFile.setFormatter( + logging.Formatter( + '[%(asctime)s] <%(name)s> (%(lineno)d): %(message)s' + ) +) +logFile.setLevel(logging.DEBUG) +logger.addHandler( + logFile +) + + +@register_importer +class PMDImporter(Importer): + # Required super 'base' class dependencies... + name = "payment_method_defaults" # Command calling name. + supported_languages = ['fi', 'sv', 'en'] # Language requirement. + data_source = None # Base data_source requirement. + organization = None # Base organization requirement. + + def setup(self: 'events.importer.payment_method_defaults.PMDImporter') -> None: + data = [ + 'Käteinen', + 'Maksukortti', + 'Virikeseteli', + 'Tyky-ranneke', + 'Verkkomaksu', + 'Mobile Pay', + 'Museokortti', + 'Lasku', + ] + + for idx, word in enumerate(data, start=1): + try: + pm = PaymentMethod() + pm.id = str(idx) + pm.name = word + pm.save() + except Exception as e: + logger.error(e) diff --git a/events/migrations/0083_auto_20211102_1005.py b/events/migrations/0083_auto_20211102_1005.py new file mode 100644 index 000000000..8f2dd3ed6 --- /dev/null +++ b/events/migrations/0083_auto_20211102_1005.py @@ -0,0 +1,33 @@ +# Generated by Django 2.2.24 on 2021-11-02 08:05 + +from django.db import migrations, models +import events.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('events', '0082_event_enrolment_url'), + ] + + operations = [ + migrations.CreateModel( + name='PaymentMethod', + fields=[ + ('id', models.CharField(max_length=100, primary_key=True, serialize=False)), + ('name', models.CharField(blank=True, max_length=25, verbose_name='Name')), + ('name_fi', models.CharField(blank=True, max_length=25, null=True, verbose_name='Name')), + ('name_sv', models.CharField(blank=True, max_length=25, null=True, verbose_name='Name')), + ('name_en', models.CharField(blank=True, max_length=25, null=True, verbose_name='Name')), + ('name_zh_hans', models.CharField(blank=True, max_length=25, null=True, verbose_name='Name')), + ('name_ru', models.CharField(blank=True, max_length=25, null=True, verbose_name='Name')), + ('name_ar', models.CharField(blank=True, max_length=25, null=True, verbose_name='Name')), + ], + bases=(models.Model, events.models.SimpleValueMixin), + ), + migrations.AddField( + model_name='offer', + name='payment_methods', + field=models.ManyToManyField(blank=True, related_name='paymentmethods', to='events.PaymentMethod'), + ), + ] diff --git a/events/models.py b/events/models.py index 5f21ac136..b3579a2ca 100644 --- a/events/models.py +++ b/events/models.py @@ -854,6 +854,14 @@ def keyword_added_or_removed(sender, model=None, instance.save(update_fields=("n_events_changed",)) +class PaymentMethod(models.Model, SimpleValueMixin): + id = models.CharField(max_length=100, primary_key=True) + name = models.CharField(verbose_name=('Name'), blank=True, max_length=25) + + def value_fields(self): + return ['name'] + + class Offer(models.Model, SimpleValueMixin): event = models.ForeignKey( Event, on_delete=models.CASCADE, db_index=True, related_name='offers') @@ -866,6 +874,8 @@ class Offer(models.Model, SimpleValueMixin): # Don't expose is_free as an API field. It is used to distinguish # between missing price info and confirmed free entry. is_free = models.BooleanField(verbose_name=_('Is free'), default=False) + payment_methods = models.ManyToManyField( + PaymentMethod, blank=True, related_name='paymentmethods') def value_fields(self): return ['price', 'info_url', 'description', 'is_free'] diff --git a/events/translation.py b/events/translation.py index fae2b30ea..a5ba595cd 100644 --- a/events/translation.py +++ b/events/translation.py @@ -1,5 +1,5 @@ from modeltranslation.translator import translator, TranslationOptions -from .models import Language, Keyword, KeywordSet, Place, Event, Offer, License, Video, Image +from .models import Language, Keyword, KeywordSet, Place, Event, Offer, License, Video, Image, PaymentMethod class LanguageTranslationOptions(TranslationOptions): @@ -24,7 +24,8 @@ class KeywordSetTranslationOptions(TranslationOptions): class PlaceTranslationOptions(TranslationOptions): - fields = ('name', 'description', 'info_url', 'street_address', 'address_locality', 'telephone') + fields = ('name', 'description', 'info_url', + 'street_address', 'address_locality', 'telephone') translator.register(Place, PlaceTranslationOptions) @@ -39,6 +40,13 @@ class EventTranslationOptions(TranslationOptions): translator.register(Event, EventTranslationOptions) +class PaymentMethodOptions(TranslationOptions): + fields = ('name',) + + +translator.register(PaymentMethod, PaymentMethodOptions) + + class OfferTranslationOptions(TranslationOptions): fields = ('price', 'info_url', 'description') @@ -63,4 +71,5 @@ class VideoTranslationOptions(TranslationOptions): class ImageTranslationOptions(TranslationOptions): fields = ('alt_text', 'name') + translator.register(Image, ImageTranslationOptions)