From 420d443906455b105258d37e2381aa8deb62a6b1 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 30 Sep 2019 17:29:37 -0400 Subject: [PATCH 1/9] Update django-cors-headers from 3.1.0 to 3.1.1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 82ceef47..7ca9beea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ Django==2.2.5 djangorestframework==3.10.3 -django-cors-headers==3.1.0 +django-cors-headers==3.1.1 django-filter==2.2.0 coreapi==2.3.3 factory-boy==2.12.0 From ab47df55e4384c9fe52ebab01c567865f1264492 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 1 Oct 2019 07:57:40 -0400 Subject: [PATCH 2/9] Update django from 2.2.5 to 2.2.6 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 82ceef47..7a9d647d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Django==2.2.5 +Django==2.2.6 djangorestframework==3.10.3 django-cors-headers==3.1.0 django-filter==2.2.0 From 79dcbc743b51aac0d74f069cd26b8249865cde2b Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 1 Oct 2019 18:43:53 -0400 Subject: [PATCH 3/9] Update pillow from 6.1.0 to 6.2.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 82ceef47..e468fa6e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ factory-boy==2.12.0 Pygments==2.4.2 Markdown==3.1.1 django-anymail==6.1.0 -Pillow==6.1.0 +Pillow==6.2.0 django-simple-history==2.7.3 djangorestframework-filters==0.11.1 python-decouple==3.1 From f75eff0c64f4fdc3fac2426d0a0e0fbf26ce42b2 Mon Sep 17 00:00:00 2001 From: willy Date: Tue, 1 Oct 2019 23:02:40 -0400 Subject: [PATCH 4/9] update charfield with max_length to Textfield model --- .../migrations/0019_auto_20191001_2257.py | 43 +++++++++++++++++++ retirement/models.py | 9 ++-- 2 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 retirement/migrations/0019_auto_20191001_2257.py diff --git a/retirement/migrations/0019_auto_20191001_2257.py b/retirement/migrations/0019_auto_20191001_2257.py new file mode 100644 index 00000000..5368bed5 --- /dev/null +++ b/retirement/migrations/0019_auto_20191001_2257.py @@ -0,0 +1,43 @@ +# Generated by Django 2.2.5 on 2019-10-02 02:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('retirement', '0018_auto_20190906_1322'), + ] + + operations = [ + migrations.AlterField( + model_name='historicalretreat', + name='carpool_url', + field=models.TextField(blank=True, null=True, verbose_name='Carpool URL'), + ), + migrations.AlterField( + model_name='historicalretreat', + name='form_url', + field=models.TextField(blank=True, null=True, verbose_name='Form URL'), + ), + migrations.AlterField( + model_name='historicalretreat', + name='review_url', + field=models.TextField(blank=True, null=True, verbose_name='Review URL'), + ), + migrations.AlterField( + model_name='retreat', + name='carpool_url', + field=models.TextField(blank=True, null=True, verbose_name='Carpool URL'), + ), + migrations.AlterField( + model_name='retreat', + name='form_url', + field=models.TextField(blank=True, null=True, verbose_name='Form URL'), + ), + migrations.AlterField( + model_name='retreat', + name='review_url', + field=models.TextField(blank=True, null=True, verbose_name='Review URL'), + ), + ] diff --git a/retirement/models.py b/retirement/models.py index 63aa64e9..26c46559 100644 --- a/retirement/models.py +++ b/retirement/models.py @@ -114,24 +114,21 @@ class Meta: accessibility = models.BooleanField(verbose_name=_("Accessibility"), ) - form_url = models.CharField( + form_url = models.TextField( blank=True, null=True, - max_length=2000, # Max URL length supported by IE verbose_name=_("Form URL"), ) - carpool_url = models.CharField( + carpool_url = models.TextField( blank=True, null=True, - max_length=2000, # Max URL length supported by IE verbose_name=_("Carpool URL"), ) - review_url = models.CharField( + review_url = models.TextField( blank=True, null=True, - max_length=2000, # Max URL length supported by IE verbose_name=_("Review URL"), ) From faf9ad8f70b92888f1f64f661607f0ffed541d46 Mon Sep 17 00:00:00 2001 From: Jerome Celle Date: Wed, 2 Oct 2019 13:35:10 +0200 Subject: [PATCH 5/9] Add send_confirm_email action in user viewset --- blitz_api/views.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/blitz_api/views.py b/blitz_api/views.py index 2d4c56f7..8d471ca0 100644 --- a/blitz_api/views.py +++ b/blitz_api/views.py @@ -179,6 +179,51 @@ def create(self, request, *args, **kwargs): return response + @action(detail=True, permission_classes=[IsAdminUser]) + def send_email_confirm(self, request, pk): + user = self.get_object() + + if settings.LOCAL_SETTINGS['EMAIL_SERVICE'] is True: + FRONTEND_SETTINGS = settings.LOCAL_SETTINGS[ + 'FRONTEND_INTEGRATION' + ] + + # Get the token of the saved user and send it with an email + activate_token = ActionToken.objects.get( + user=user, + type='account_activation', + ) + + activate_token.expires = timezone.now() + timezone.timedelta( + minutes=settings.ACTIVATION_TOKENS['MINUTES'] + ) + + # Setup the url for the activation button in the email + activation_url = FRONTEND_SETTINGS['ACTIVATION_URL'].replace( + "{{token}}", + activate_token.key + ) + + response_send_mail = services.send_mail( + [user], + { + "activation_url": activation_url, + "first_name": user.first_name, + "last_name": user.last_name, + }, + "CONFIRM_SIGN_UP", + ) + + if response_send_mail: + content = { + 'detail': _("The account was created but no email was " + "sent. If your account is not " + "activated, contact the administration."), + } + return Response(content, status=status.HTTP_200_OK) + + return Response(status=status.HTTP_200_OK) + class UsersActivation(APIView): """ From 801bc7638f01cbed557bd925974cdd027d6e55ab Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 2 Oct 2019 16:00:42 -0400 Subject: [PATCH 6/9] Update awscli from 1.16.240 to 1.16.251 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 9f24f0b1..5cf25c94 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,4 +2,4 @@ pycodestyle==2.5.0 coveralls==1.8.2 responses==0.10.6 six==1.12.0 -awscli==1.16.240 +awscli==1.16.251 From 1b91d4ceaa12bdf76b68887e0ca9581974fecbd8 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 3 Oct 2019 15:15:45 -0400 Subject: [PATCH 7/9] Update awscli from 1.16.251 to 1.16.252 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 5cf25c94..e4f42739 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,4 +2,4 @@ pycodestyle==2.5.0 coveralls==1.8.2 responses==0.10.6 six==1.12.0 -awscli==1.16.251 +awscli==1.16.252 From 4f2ce77953484a9e6d97f2a76669bd1f190388b7 Mon Sep 17 00:00:00 2001 From: Jerome Celle Date: Fri, 4 Oct 2019 10:15:45 +0200 Subject: [PATCH 8/9] Add option to reserved seats with invitation --- .../migrations/0020_auto_20191004_0256.py | 23 +++++ retirement/models.py | 43 +++++--- retirement/tests/test_viewset_Invitation.py | 30 ++++++ store/serializers.py | 14 +-- store/tests/tests_viewset_Order.py | 97 ++++++++++++++++++- 5 files changed, 180 insertions(+), 27 deletions(-) create mode 100644 retirement/migrations/0020_auto_20191004_0256.py diff --git a/retirement/migrations/0020_auto_20191004_0256.py b/retirement/migrations/0020_auto_20191004_0256.py new file mode 100644 index 00000000..47d1caa6 --- /dev/null +++ b/retirement/migrations/0020_auto_20191004_0256.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.5 on 2019-10-04 06:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('retirement', '0019_auto_20191001_2257'), + ] + + operations = [ + migrations.AddField( + model_name='historicalretreatinvitation', + name='reserve_seat', + field=models.BooleanField(default=False, verbose_name='Should reserve seat'), + ), + migrations.AddField( + model_name='retreatinvitation', + name='reserve_seat', + field=models.BooleanField(default=False, verbose_name='Should reserve seat'), + ), + ] diff --git a/retirement/models.py b/retirement/models.py index 26c46559..d7ea5750 100644 --- a/retirement/models.py +++ b/retirement/models.py @@ -184,23 +184,32 @@ class Meta: @property def total_reservations(self): - reservations = Reservation.objects.filter( - retreat=self, - is_active=True, - ).count() - return reservations + return self.reservations.filter(is_active=True).count() @property def places_remaining(self): - seats = self.seats - reserved_seats = self.reserved_seats - reservations = self.reservations.filter(is_active=True).count() - return seats - reservations - reserved_seats + # Nb places available without invitations + seat_remaining = \ + self.seats - self.total_reservations - self.reserved_seats - def has_places_remaining(self): - return (self.seats - - self.total_reservations - - self.reserved_seats) > 0 + # Remove places reserved by invitations + for invitation in self.invitations.all(): + if invitation.reserve_seat: + seat_remaining = \ + seat_remaining - invitation.nb_places_free() + + return seat_remaining + + def has_places_remaining(self, selected_invitation=None): + seat_remaining = self.places_remaining + + # add places reserved for the selected invitation + if selected_invitation and \ + selected_invitation.reserve_seat: + seat_remaining = \ + seat_remaining + selected_invitation.nb_places_free() + + return seat_remaining > 0 def __str__(self): return self.name @@ -546,6 +555,11 @@ class RetreatInvitation(SafeDeleteModel): blank=True ) + reserve_seat = models.BooleanField( + verbose_name=_("Should reserve seat"), + default=False + ) + history = HistoricalRecords() def __str__(self): @@ -574,5 +588,8 @@ def nb_places_used(self): return self.retreat_reservations.filter(is_active=True).count() + def nb_places_free(self): + return self.nb_places - self.nb_places_used + def has_free_places(self): return self.nb_places_used < self.nb_places diff --git a/retirement/tests/test_viewset_Invitation.py b/retirement/tests/test_viewset_Invitation.py index 63585baa..574d9ddb 100644 --- a/retirement/tests/test_viewset_Invitation.py +++ b/retirement/tests/test_viewset_Invitation.py @@ -127,3 +127,33 @@ def test_create(self): response_data.get('front_url'), url ) + + def test_create_with_reserved_seat_option(self): + + self.client.force_authenticate(user=self.admin) + + data = { + 'retreat': reverse('retreat:retreat-detail', + args=[self.retreat.id], + request=self.request), + 'nb_places': 4, + 'reserve_seat': True + } + + response = self.client.post( + reverse('retreat:retreatinvitation-list'), + data, + format='json', + ) + + self.assertEqual( + response.status_code, + status.HTTP_201_CREATED, + response.content, + ) + + self.retreat.refresh_from_db() + self.assertEqual( + self.retreat.places_remaining, + self.retreat.seats - 4 + ) diff --git a/store/serializers.py b/store/serializers.py index 2307b8cc..9cad2b64 100644 --- a/store/serializers.py +++ b/store/serializers.py @@ -747,23 +747,15 @@ def create(self, validated_data): "retreat: {0}.".format(str(retreat)) )] }) - if ((retreat.has_places_remaining()) + + invitation = retreat_orderline.get_invitation() + if ((retreat.has_places_remaining(invitation)) or (retreat.reserved_seats and WaitQueueNotification.objects.filter( user=user, retreat=retreat))): # Manage invitation to retreat - # If there are no places left we raise an error # The invitation id is store in the orderline metadata - invitation = retreat_orderline.get_invitation() - - if invitation and not invitation.has_free_places(): - raise serializers.ValidationError({ - 'invitation_id': [_( - "There are no places left for " - "the requested invitation." - )] - }) new_retreat_reservation = \ RetreatReservation.objects.create( diff --git a/store/tests/tests_viewset_Order.py b/store/tests/tests_viewset_Order.py index c5fdc529..07c2d434 100644 --- a/store/tests/tests_viewset_Order.py +++ b/store/tests/tests_viewset_Order.py @@ -221,7 +221,8 @@ def setUp(self): self.invitation = RetreatInvitation.objects.create( retreat=self.retreat, - nb_places=5 + nb_places=5, + reserve_seat=True ) self.maxDiff = None @@ -415,6 +416,94 @@ def test_create_with_payment_token(self): self.assertEqual(new_order.total_cost, total_price) + @responses.activate + def test_order_retreat_invitation_reserved_seats(self): + """ + Ensure we can create an order when provided with a payment_token. + (Token representing an existing payment card.) + """ + FIXED_TIME = datetime(2018, 1, 1, tzinfo=LOCAL_TIMEZONE) + + self.client.force_authenticate(user=self.admin) + + responses.add( + responses.POST, + "http://example.com/cardpayments/v1/accounts/0123456789/auths/", + json=SAMPLE_PAYMENT_RESPONSE, + status=200 + ) + + self.retreat.seats = self.invitation.nb_places_free() + self.retreat.save() + + data = { + 'payment_token': "CZgD1NlBzPuSefg", + 'order_lines': [{ + 'content_type': 'retreat', + 'object_id': self.retreat.id, + 'quantity': 1, + 'options': [{ + 'id': self.options.id, + 'quantity': 1 + }] + }], + } + + with mock.patch( + 'store.serializers.timezone.now', return_value=FIXED_TIME): + response = self.client.post( + reverse('order-list'), + data, + format='json', + ) + + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST, + response.content, + ) + + response_data = json.loads(response.content) + data = { + "non_field_errors": [ + "There are no places left in the requested retreat."] + } + + self.assertEqual( + response_data, + data, + response_data, + ) + + data = { + 'payment_token': "CZgD1NlBzPuSefg", + 'order_lines': [{ + 'content_type': 'retreat', + 'object_id': self.retreat.id, + 'quantity': 1, + 'metadata': + json.dumps({'invitation_id': self.invitation.id}), + 'options': [{ + 'id': self.options.id, + 'quantity': 1 + }] + }], + } + + with mock.patch( + 'store.serializers.timezone.now', return_value=FIXED_TIME): + response = self.client.post( + reverse('order-list'), + data, + format='json', + ) + + self.assertEqual( + response.status_code, + status.HTTP_201_CREATED, + response.content, + ) + @responses.activate def test_buy_renew_membership(self): """ @@ -2393,9 +2482,11 @@ def test_create_with_membership_coupon(self): self.assertEqual(self.admin.coupons.all().count(), 1 + nb_coupon_start) - new_coupon = self.admin.coupons.all()[0] + # Get the last coupon generate, it should be the new one associate + # with the membership + new_coupon = self.admin.coupons.all().order_by('-id')[0] - self.assertEqual(new_coupon.value, 100) + self.assertEqual(new_coupon.value, membership_coupon.value) self.assertEqual(new_coupon.percent_off, 0) self.assertEqual(new_coupon.max_use, 4) self.assertEqual(new_coupon.max_use_per_user, 4) From 6fc167268697ac15028756f465ee930e38959f0c Mon Sep 17 00:00:00 2001 From: Jerome Celle Date: Fri, 4 Oct 2019 18:21:22 +0200 Subject: [PATCH 9/9] Add Log management and generate log with email and payment --- blitz_api/services.py | 76 +++++++++++--- blitz_api/settings.py | 1 + log_management/__init__.py | 0 log_management/admin.py | 19 ++++ log_management/apps.py | 6 ++ log_management/migrations/0001_initial.py | 30 ++++++ log_management/migrations/__init__.py | 0 log_management/models.py | 69 +++++++++++++ log_management/tests/__init__.py | 0 log_management/tests/test_logs_model.py | 23 +++++ retirement/models.py | 31 ++++-- retirement/serializers.py | 88 ++++++++++++---- retirement/services.py | 90 +++++++++++++---- retirement/views.py | 31 ++++-- store/models.py | 31 ++++-- store/serializers.py | 30 ++++-- store/services.py | 117 ++++++++++++++-------- utils/deploy_with_docker.sh | 2 - workplace/serializers.py | 32 ++++-- workplace/views.py | 61 ++++++++--- 20 files changed, 587 insertions(+), 150 deletions(-) create mode 100644 log_management/__init__.py create mode 100644 log_management/admin.py create mode 100644 log_management/apps.py create mode 100644 log_management/migrations/0001_initial.py create mode 100644 log_management/migrations/__init__.py create mode 100644 log_management/models.py create mode 100644 log_management/tests/__init__.py create mode 100644 log_management/tests/test_logs_model.py diff --git a/blitz_api/services.py b/blitz_api/services.py index 75bf7512..54c4cbda 100644 --- a/blitz_api/services.py +++ b/blitz_api/services.py @@ -1,3 +1,4 @@ +import json from datetime import datetime import pytz @@ -12,6 +13,7 @@ from rest_framework.pagination import PageNumberPagination +from log_management.models import Log from .exceptions import MailServiceError from django.core.mail import send_mail as django_send_mail @@ -43,7 +45,21 @@ def send_mail(users, context, template): # use this SendinBlue template message.template_id = MAIL_SERVICE["TEMPLATES"].get(template) message.merge_global_data = context - response = message.send() # return number of successfully sent emails + try: + # return number of successfully sent emails + response = message.send() + except Exception as err: + additional_data = { + 'email': user.email, + 'context': context, + 'template': template + } + Log.error( + source='SENDING_BLUE_TEMPLATE', + message=err, + additional_data=json.dumps(additional_data) + ) + raise if not response: failed_emails.append(user.email) @@ -136,13 +152,28 @@ def notify_user_of_new_account(email, password): merge_data ) - return django_send_mail( - "Bienvenue à Thèsez-vous?", - plain_msg, - settings.DEFAULT_FROM_EMAIL, - [email], - html_message=msg_html, - ) + try: + return django_send_mail( + "Bienvenue à Thèsez-vous?", + plain_msg, + settings.DEFAULT_FROM_EMAIL, + [email], + html_message=msg_html, + ) + except Exception as err: + additional_data = { + 'title': "Bienvenue à Thèsez-vous?", + 'default_from': settings.DEFAULT_FROM_EMAIL, + 'user_email': email, + 'merge_data': merge_data, + 'template': 'notify_user_of_new_account' + } + Log.error( + source='SENDING_BLUE_TEMPLATE', + message=err, + additional_data=json.dumps(additional_data) + ) + raise def notify_user_of_change_email(email, activation_url, first_name): @@ -163,13 +194,28 @@ def notify_user_of_change_email(email, activation_url, first_name): merge_data ) - return django_send_mail( - "Confirmation de votre nouvelle adresse courriel", - plain_msg, - settings.DEFAULT_FROM_EMAIL, - [email], - html_message=msg_html, - ) + try: + return django_send_mail( + "Confirmation de votre nouvelle adresse courriel", + plain_msg, + settings.DEFAULT_FROM_EMAIL, + [email], + html_message=msg_html, + ) + except Exception as err: + additional_data = { + 'title': "Confirmation de votre nouvelle adresse courriel", + 'default_from': settings.DEFAULT_FROM_EMAIL, + 'user_email': email, + 'merge_data': merge_data, + 'template': 'notify_user_of_change_email' + } + Log.error( + source='SENDING_BLUE_TEMPLATE', + message=err, + additional_data=json.dumps(additional_data) + ) + raise class ExportPagination(PageNumberPagination): diff --git a/blitz_api/settings.py b/blitz_api/settings.py index 2f6692c5..8a1212a8 100644 --- a/blitz_api/settings.py +++ b/blitz_api/settings.py @@ -50,6 +50,7 @@ 'workplace', 'store', 'retirement', + 'log_management', 'storages', 'anymail', 'simple_history', diff --git a/log_management/__init__.py b/log_management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/log_management/admin.py b/log_management/admin.py new file mode 100644 index 00000000..d1814c2a --- /dev/null +++ b/log_management/admin.py @@ -0,0 +1,19 @@ + +from django.contrib import admin + +from log_management.models import Log + + +class LogAdmin(admin.ModelAdmin): + list_display = ('source', 'level', 'error_code', 'message',) + search_fields = ( + 'message', 'additional_data', 'level', 'source', 'error_code', + 'traceback_data') + list_filter = ( + 'level', + 'source', + 'error_code', + ) + + +admin.site.register(Log, LogAdmin) diff --git a/log_management/apps.py b/log_management/apps.py new file mode 100644 index 00000000..dbd93c8b --- /dev/null +++ b/log_management/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class LogManagementConfig(AppConfig): + name = 'log_management' + verbose_name = 'Log' diff --git a/log_management/migrations/0001_initial.py b/log_management/migrations/0001_initial.py new file mode 100644 index 00000000..28a3a02a --- /dev/null +++ b/log_management/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# Generated by Django 2.2.5 on 2019-10-03 10:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Log', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('source', models.CharField(max_length=100, verbose_name='Source')), + ('level', models.CharField(max_length=100, verbose_name='Level')), + ('message', models.TextField(verbose_name='Message')), + ('error_code', models.CharField(blank=True, max_length=100, null=True, verbose_name='Error Code')), + ('additional_data', models.TextField(blank=True, null=True, verbose_name='Additional data')), + ('traceback_data', models.TextField(blank=True, null=True, verbose_name='TraceBack')), + ], + options={ + 'verbose_name': 'Log', + 'verbose_name_plural': 'Logs', + }, + ), + ] diff --git a/log_management/migrations/__init__.py b/log_management/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/log_management/models.py b/log_management/models.py new file mode 100644 index 00000000..f7bb74e2 --- /dev/null +++ b/log_management/models.py @@ -0,0 +1,69 @@ +import json +import traceback + +from django.db import models + +from django.utils.translation import ugettext_lazy as _ + + +class Log(models.Model): + + LEVEL_ERROR = 'ERROR' + LEVEL_INFO = 'INFO' + LEVEL_DEBUG = 'DEBUG' + + source = models.CharField( + max_length=100, + verbose_name=_("Source"), + ) + + level = models.CharField( + max_length=100, + verbose_name=_("Level"), + ) + + message = models.TextField( + verbose_name=_("Message"), + ) + + error_code = models.CharField( + blank=True, + null=True, + max_length=100, + verbose_name=_("Error Code"), + ) + + additional_data = models.TextField( + blank=True, + null=True, + verbose_name=_("Additional data"), + ) + + traceback_data = models.TextField( + blank=True, + null=True, + verbose_name=_("TraceBack"), + ) + + class Meta: + verbose_name = _("Log") + verbose_name_plural = _("Logs") + + @classmethod + def error(cls, source, message, error_code=None, additional_data=None): + traceback_data = ''.join(traceback.format_stack(limit=10)) + new_log = Log( + level=cls.LEVEL_ERROR, + source=source, + message=message, + traceback_data=traceback_data + ) + + if error_code: + new_log.error_code = error_code + if additional_data: + new_log.additional_data = additional_data + + new_log.save() + + return new_log diff --git a/log_management/tests/__init__.py b/log_management/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/log_management/tests/test_logs_model.py b/log_management/tests/test_logs_model.py new file mode 100644 index 00000000..9aaf3de0 --- /dev/null +++ b/log_management/tests/test_logs_model.py @@ -0,0 +1,23 @@ +import json + +from django.test import TestCase + +from log_management.models import Log + + +class LogsTests(TestCase): + def test_create_error(self): + additional_data = { + 'title': "Place exclusive pour 24h", + 'default_from': 'from@email', + 'user_email': 'user.email', + 'merge_data': 'merge_data', + 'template': 'reserved_place' + } + new_log = Log.error( + source='SENDING_BLUE_TEMPLATE', + message='err', + additional_data=json.dumps(additional_data) + ) + + self.assertEqual(new_log.source, 'SENDING_BLUE_TEMPLATE') diff --git a/retirement/models.py b/retirement/models.py index d7ea5750..e4e6ce2d 100644 --- a/retirement/models.py +++ b/retirement/models.py @@ -20,6 +20,8 @@ from django.utils.translation import ugettext_lazy as _ from safedelete.models import SafeDeleteModel from simple_history.models import HistoricalRecords + +from log_management.models import Log from store.models import Membership, OrderLine, BaseProduct,\ Coupon @@ -316,13 +318,28 @@ def notify_reserved_seat(self, user): plain_msg = render_to_string("reserved_place.txt", merge_data) msg_html = render_to_string("reserved_place.html", merge_data) - return send_mail( - "Place exclusive pour 24h", - plain_msg, - settings.DEFAULT_FROM_EMAIL, - [user.email], - html_message=msg_html, - ) + try: + return send_mail( + "Place exclusive pour 24h", + plain_msg, + settings.DEFAULT_FROM_EMAIL, + [user.email], + html_message=msg_html, + ) + except Exception as err: + additional_data = { + 'title': "Place exclusive pour 24h", + 'default_from': settings.DEFAULT_FROM_EMAIL, + 'user_email': user.email, + 'merge_data': merge_data, + 'template': 'reserved_place' + } + Log.error( + source='SENDING_BLUE_TEMPLATE', + message=err, + additional_data=json.dumps(additional_data) + ) + raise class Picture(models.Model): diff --git a/retirement/serializers.py b/retirement/serializers.py index 112b5684..93ae17e0 100644 --- a/retirement/serializers.py +++ b/retirement/serializers.py @@ -22,6 +22,7 @@ from blitz_api.services import (check_if_translated_field, remove_translation_fields, getMessageTranslate) +from log_management.models import Log from store.exceptions import PaymentAPIError from store.models import Order, OrderLine, PaymentProfile, Refund from store.serializers import BaseProductSerializer, CouponSerializer @@ -825,13 +826,28 @@ def update(self, instance, validated_data): plain_msg = render_to_string("refund.txt", merge_data) msg_html = render_to_string("refund.html", merge_data) - send_mail( - "Confirmation de remboursement", - plain_msg, - settings.DEFAULT_FROM_EMAIL, - [user.email], - html_message=msg_html, - ) + try: + send_mail( + "Confirmation de remboursement", + plain_msg, + settings.DEFAULT_FROM_EMAIL, + [user.email], + html_message=msg_html, + ) + except Exception as err: + additional_data = { + 'title': "Confirmation de remboursement", + 'default_from': settings.DEFAULT_FROM_EMAIL, + 'user_email': user.email, + 'merge_data': merge_data, + 'template': 'refund' + } + Log.error( + source='SENDING_BLUE_TEMPLATE', + message=err, + additional_data=json.dumps(additional_data) + ) + raise # Send exchange confirmation email if validated_data.get('retreat'): @@ -853,13 +869,28 @@ def update(self, instance, validated_data): plain_msg = render_to_string("exchange.txt", merge_data) msg_html = render_to_string("exchange.html", merge_data) - send_mail( - "Confirmation d'échange", - plain_msg, - settings.DEFAULT_FROM_EMAIL, - [user.email], - html_message=msg_html, - ) + try: + send_mail( + "Confirmation d'échange", + plain_msg, + settings.DEFAULT_FROM_EMAIL, + [user.email], + html_message=msg_html, + ) + except Exception as err: + additional_data = { + 'title': "Confirmation d'échange", + 'default_from': settings.DEFAULT_FROM_EMAIL, + 'user_email': user.email, + 'merge_data': merge_data, + 'template': 'exchange' + } + Log.error( + source='SENDING_BLUE_TEMPLATE', + message=err, + additional_data=json.dumps(additional_data) + ) + raise merge_data = { 'RETREAT': new_retreat, @@ -875,13 +906,28 @@ def update(self, instance, validated_data): merge_data ) - send_mail( - "Confirmation d'inscription à la retraite", - plain_msg, - settings.DEFAULT_FROM_EMAIL, - [instance.user.email], - html_message=msg_html, - ) + try: + send_mail( + "Confirmation d'inscription à la retraite", + plain_msg, + settings.DEFAULT_FROM_EMAIL, + [instance.user.email], + html_message=msg_html, + ) + except Exception as err: + additional_data = { + 'title': "Confirmation d'inscription à la retraite", + 'default_from': settings.DEFAULT_FROM_EMAIL, + 'user_email': user.email, + 'merge_data': merge_data, + 'template': 'retreat_info' + } + Log.error( + source='SENDING_BLUE_TEMPLATE', + message=err, + additional_data=json.dumps(additional_data) + ) + raise return Reservation.objects.get(id=instance_pk) diff --git a/retirement/services.py b/retirement/services.py index 60a4cb69..fc426bac 100644 --- a/retirement/services.py +++ b/retirement/services.py @@ -1,3 +1,4 @@ +import json from decimal import Decimal from django.conf import settings @@ -5,6 +6,7 @@ from django.template.loader import render_to_string from django.utils import timezone +from log_management.models import Log from retirement.models import WaitQueue from store.models import Refund from store.services import (PAYSAFE_EXCEPTION, @@ -36,13 +38,29 @@ def notify_reserved_retreat_seat(user, retreat): plain_msg = render_to_string("reserved_place.txt", merge_data) msg_html = render_to_string("reserved_place.html", merge_data) - return send_mail( - "Place exclusive pour 24h", - plain_msg, - settings.DEFAULT_FROM_EMAIL, - [user.email], - html_message=msg_html, - ) + try: + return send_mail( + "Place exclusive pour 24h", + plain_msg, + settings.DEFAULT_FROM_EMAIL, + [user.email], + html_message=msg_html, + ) + + except Exception as err: + additional_data = { + 'title': "Place exclusive pour 24h", + 'default_from': settings.DEFAULT_FROM_EMAIL, + 'user_email': user.email, + 'merge_data': merge_data, + 'template': 'reserved_place' + } + Log.error( + source='SENDING_BLUE_TEMPLATE', + message=err, + additional_data=json.dumps(additional_data) + ) + raise def send_retreat_7_days_email(user, retreat): @@ -56,13 +74,28 @@ def send_retreat_7_days_email(user, retreat): plain_msg = render_to_string("reminder.txt", merge_data) msg_html = render_to_string("reminder.html", merge_data) - return send_mail( - "Rappel retraite", - plain_msg, - settings.DEFAULT_FROM_EMAIL, - [user.email], - html_message=msg_html, - ) + try: + return send_mail( + "Rappel retraite", + plain_msg, + settings.DEFAULT_FROM_EMAIL, + [user.email], + html_message=msg_html, + ) + except Exception as err: + additional_data = { + 'title': "Rappel retraite", + 'default_from': settings.DEFAULT_FROM_EMAIL, + 'user_email': user.email, + 'merge_data': merge_data, + 'template': 'reminder' + } + Log.error( + source='SENDING_BLUE_TEMPLATE', + message=err, + additional_data=json.dumps(additional_data) + ) + raise def send_post_retreat_email(user, retreat): @@ -79,13 +112,28 @@ def send_post_retreat_email(user, retreat): plain_msg = render_to_string("throwback.txt", merge_data) msg_html = render_to_string("throwback.html", merge_data) - return send_mail( - "Merci pour votre participation", - plain_msg, - settings.DEFAULT_FROM_EMAIL, - [user.email], - html_message=msg_html, - ) + try: + return send_mail( + "Merci pour votre participation", + plain_msg, + settings.DEFAULT_FROM_EMAIL, + [user.email], + html_message=msg_html, + ) + except Exception as err: + additional_data = { + 'title': "Merci pour votre participation", + 'default_from': settings.DEFAULT_FROM_EMAIL, + 'user_email': user.email, + 'merge_data': merge_data, + 'template': 'throwback' + } + Log.error( + source='SENDING_BLUE_TEMPLATE', + message=err, + additional_data=json.dumps(additional_data) + ) + raise def refund_retreat(reservation, refund_rate, refund_reason): diff --git a/retirement/views.py b/retirement/views.py index 6fda5a0d..9a9ea286 100644 --- a/retirement/views.py +++ b/retirement/views.py @@ -1,3 +1,4 @@ +import json from decimal import Decimal from datetime import datetime, timedelta @@ -21,6 +22,7 @@ from blitz_api.models import ExportMedia from blitz_api.serializers import ExportMediaSerializer +from log_management.models import Log from store.exceptions import PaymentAPIError from store.models import Refund, OptionProduct, OrderLineBaseProduct from store.services import refund_amount, PAYSAFE_EXCEPTION @@ -486,13 +488,28 @@ def send_refund_confirmation_email(self, amount, retreat, order, user, plain_msg = render_to_string("refund.txt", merge_data) msg_html = render_to_string("refund.html", merge_data) - django_send_mail( - "Confirmation de remboursement", - plain_msg, - settings.DEFAULT_FROM_EMAIL, - [user.email], - html_message=msg_html, - ) + try: + django_send_mail( + "Confirmation de remboursement", + plain_msg, + settings.DEFAULT_FROM_EMAIL, + [user.email], + html_message=msg_html, + ) + except Exception as err: + additional_data = { + 'title': "Confirmation de votre nouvelle adresse courriel", + 'default_from': settings.DEFAULT_FROM_EMAIL, + 'user_email': user.email, + 'merge_data': merge_data, + 'template': 'notify_user_of_change_email' + } + Log.error( + source='SENDING_BLUE_TEMPLATE', + message=err, + additional_data=json.dumps(additional_data) + ) + raise class WaitQueueViewSet(ExportMixin, viewsets.ModelViewSet): diff --git a/store/models.py b/store/models.py index 3abb9101..bef9891c 100644 --- a/store/models.py +++ b/store/models.py @@ -23,6 +23,8 @@ from model_utils.managers import InheritanceManager +from log_management.models import Log + User = get_user_model() TAX_RATE = settings.LOCAL_SETTINGS['SELLING_TAX'] @@ -109,13 +111,28 @@ def send_invoice(to, merge_data): plain_msg = render_to_string("invoice.txt", merge_data) msg_html = render_to_string("invoice.html", merge_data) - send_mail( - "Confirmation d'achat", - plain_msg, - settings.DEFAULT_FROM_EMAIL, - to, - html_message=msg_html, - ) + try: + send_mail( + "Confirmation d'achat", + plain_msg, + settings.DEFAULT_FROM_EMAIL, + to, + html_message=msg_html, + ) + except Exception as err: + additional_data = { + 'title': "Confirmation d'achat", + 'default_from': settings.DEFAULT_FROM_EMAIL, + 'user_email': to, + 'merge_data': merge_data, + 'template': 'invoice' + } + Log.error( + source='SENDING_BLUE_TEMPLATE', + message=err, + additional_data=json.dumps(additional_data) + ) + raise def applying_coupon(self, coupon, user): from store.services import validate_coupon_for_order diff --git a/store/serializers.py b/store/serializers.py index 9cad2b64..fd0c2ec7 100644 --- a/store/serializers.py +++ b/store/serializers.py @@ -29,6 +29,7 @@ from blitz_api.services import (remove_translation_fields, check_if_translated_field, getMessageTranslate) +from log_management.models import Log from workplace.models import Reservation from retirement.models import Reservation as RetreatReservation, \ RetreatInvitation @@ -928,13 +929,28 @@ def create(self, validated_data): merge_data ) - send_mail( - "Confirmation d'inscription à la retraite", - plain_msg, - settings.DEFAULT_FROM_EMAIL, - [retreat_reservation.user.email], - html_message=msg_html, - ) + try: + send_mail( + "Confirmation d'inscription à la retraite", + plain_msg, + settings.DEFAULT_FROM_EMAIL, + [retreat_reservation.user.email], + html_message=msg_html, + ) + except Exception as err: + additional_data = { + 'title': "Confirmation d'inscription à la retraite", + 'default_from': settings.DEFAULT_FROM_EMAIL, + 'user_email': retreat_reservation.user.email, + 'merge_data': merge_data, + 'template': 'retreat_info' + } + Log.error( + source='SENDING_BLUE_TEMPLATE', + message=err, + additional_data=json.dumps(additional_data) + ) + raise return order diff --git a/store/services.py b/store/services.py index ca475c91..cae6377e 100644 --- a/store/services.py +++ b/store/services.py @@ -11,6 +11,7 @@ from django.utils import timezone from django.utils.translation import ugettext_lazy as _ +from log_management.models import Log from .exceptions import PaymentAPIError from .models import CouponUser @@ -96,6 +97,31 @@ } +def manage_paysafe_error(err, additional_data): + try: + err_code = json.loads(err.response.content)['error']['code'] + + Log.error( + source='PAYSAFE', + error_code=err_code, + message=err, + additional_data=json.dumps(additional_data) + ) + + if err_code in PAYSAFE_EXCEPTION: + raise PaymentAPIError(PAYSAFE_EXCEPTION[err_code]) + except json.decoder.JSONDecodeError as err: + print(err.response) + + Log.error( + source='PAYSAFE', + error_code='unknown', + message=err, + additional_data=json.dumps(additional_data) + ) + raise PaymentAPIError(PAYSAFE_EXCEPTION['unknown']) + + def charge_payment(amount, payment_token, reference_number): """ This method is used to charge an amount to a card represented by the @@ -133,14 +159,11 @@ def charge_payment(amount, payment_token, reference_number): ) r.raise_for_status() except requests.exceptions.HTTPError as err: - try: - err_code = json.loads(err.response.content)['error']['code'] - if err_code in PAYSAFE_EXCEPTION: - raise PaymentAPIError(PAYSAFE_EXCEPTION[err_code]) - except json.decoder.JSONDecodeError as err: - print(err.response) - raise PaymentAPIError(PAYSAFE_EXCEPTION['unknown']) - + manage_paysafe_error(err, { + 'amount': amount, + 'payment_token': payment_token, + 'reference_number': reference_number + }) return r @@ -178,13 +201,10 @@ def refund_amount(settlement_id, amount): ) r.raise_for_status() except requests.exceptions.HTTPError as err: - try: - err_code = json.loads(err.response.content)['error']['code'] - if err_code in PAYSAFE_EXCEPTION: - raise PaymentAPIError(PAYSAFE_EXCEPTION[err_code]) - except json.decoder.JSONDecodeError as err: - print(err.response) - raise PaymentAPIError(PAYSAFE_EXCEPTION['unknown']) + manage_paysafe_error(err, { + 'settlement_id': settlement_id, + 'amount': amount, + }) return r @@ -223,10 +243,9 @@ def create_external_payment_profile(user): ) r.raise_for_status() except requests.exceptions.HTTPError as err: - err_code = json.loads(err.response.content)['error']['code'] - if err_code in PAYSAFE_EXCEPTION: - raise PaymentAPIError(PAYSAFE_EXCEPTION[err_code]) - raise PaymentAPIError(PAYSAFE_EXCEPTION['unknown']) + manage_paysafe_error(err, { + 'data': data, + }) return r @@ -253,10 +272,9 @@ def get_external_payment_profile(profile_id): ) r.raise_for_status() except requests.exceptions.HTTPError as err: - err_code = json.loads(err.response.content)['error']['code'] - if err_code in PAYSAFE_EXCEPTION: - raise PaymentAPIError(PAYSAFE_EXCEPTION[err_code]) - raise PaymentAPIError(PAYSAFE_EXCEPTION['unknown']) + manage_paysafe_error(err, { + 'profile_id': profile_id, + }) return r @@ -290,10 +308,11 @@ def update_external_card(profile_id, card_id, single_use_token): ) r.raise_for_status() except requests.exceptions.HTTPError as err: - err_code = json.loads(err.response.content)['error']['code'] - if err_code in PAYSAFE_EXCEPTION: - raise PaymentAPIError(PAYSAFE_EXCEPTION[err_code]) - raise PaymentAPIError(PAYSAFE_EXCEPTION['unknown']) + manage_paysafe_error(err, { + 'profile_id': profile_id, + 'card_id': card_id, + 'single_use_token': single_use_token, + }) return r @@ -405,10 +424,9 @@ def get_external_card(card_id): ) r.raise_for_status() except requests.exceptions.HTTPError as err: - err_code = json.loads(err.response.content)['error']['code'] - if err_code in PAYSAFE_EXCEPTION: - raise PaymentAPIError(PAYSAFE_EXCEPTION[err_code]) - raise PaymentAPIError(PAYSAFE_EXCEPTION['unknown']) + manage_paysafe_error(err, { + 'card_id': card_id + }) return r @@ -436,10 +454,10 @@ def delete_external_card(profile_id, card_id): ) r.raise_for_status() except requests.exceptions.HTTPError as err: - err_code = json.loads(err.response.content)['error']['code'] - if err_code in PAYSAFE_EXCEPTION: - raise PaymentAPIError(PAYSAFE_EXCEPTION[err_code]) - raise PaymentAPIError(PAYSAFE_EXCEPTION['unknown']) + manage_paysafe_error(err, { + 'profile_id': profile_id, + 'card_id': card_id + }) return r @@ -574,10 +592,25 @@ def notify_for_coupon(email, coupon): plain_msg = render_to_string("coupon_code.txt", merge_data) msg_html = render_to_string("coupon_code.html", merge_data) - return send_mail( - "Coupon rabais", - plain_msg, - settings.DEFAULT_FROM_EMAIL, - [email], - html_message=msg_html, - ) + try: + return send_mail( + "Coupon rabais", + plain_msg, + settings.DEFAULT_FROM_EMAIL, + [email], + html_message=msg_html, + ) + except Exception as err: + additional_data = { + 'title': "Coupon rabais", + 'default_from': settings.DEFAULT_FROM_EMAIL, + 'user_email': email, + 'merge_data': merge_data, + 'template': 'coupon_code' + } + Log.error( + source='SENDING_BLUE_TEMPLATE', + message=err, + additional_data=json.dumps(additional_data) + ) + raise diff --git a/utils/deploy_with_docker.sh b/utils/deploy_with_docker.sh index f41b2832..9372af69 100644 --- a/utils/deploy_with_docker.sh +++ b/utils/deploy_with_docker.sh @@ -1,7 +1,5 @@ #!/bin/bash docker-compose run --rm \ --e AWS_ACCESS_KEY_ID=$1 \ --e AWS_SECRET_ACCESS_KEY=$2 \ api \ bash -c "bash ./utils/deploy_prod.sh" diff --git a/workplace/serializers.py b/workplace/serializers.py index a65d207a..eca2484a 100644 --- a/workplace/serializers.py +++ b/workplace/serializers.py @@ -1,3 +1,4 @@ +import json from copy import copy from datetime import datetime @@ -24,6 +25,7 @@ from blitz_api.services import (remove_translation_fields, check_if_translated_field, getMessageTranslate,) +from log_management.models import Log from .models import Workplace, Picture, Period, TimeSlot, Reservation from .fields import TimezoneField @@ -468,13 +470,29 @@ def update(self, instance, validated_data): "cancelation.html", merge_data ) - send_mail( - "Annulation d'un bloc de rédaction", - plain_msg, - settings.DEFAULT_FROM_EMAIL, - [reservation.user.email], - html_message=msg_html, - ) + + try: + send_mail( + "Annulation d'un bloc de rédaction", + plain_msg, + settings.DEFAULT_FROM_EMAIL, + [reservation.user.email], + html_message=msg_html, + ) + except Exception as err: + additional_data = { + 'title': "Annulation d'un bloc de rédaction", + 'default_from': settings.DEFAULT_FROM_EMAIL, + 'user_email': reservation.user.email, + 'merge_data': merge_data, + 'template': 'cancelation' + } + Log.error( + source='SENDING_BLUE_TEMPLATE', + message=err, + additional_data=json.dumps(additional_data) + ) + raise return super(TimeSlotSerializer, self).update( instance, diff --git a/workplace/views.py b/workplace/views.py index e716c0b0..aa2713e5 100644 --- a/workplace/views.py +++ b/workplace/views.py @@ -22,9 +22,11 @@ from django.template.loader import render_to_string from django.utils import timezone from django.utils.translation import ugettext_lazy as _ +from rest_framework.utils import json from blitz_api.exceptions import MailServiceError from blitz_api.mixins import ExportMixin +from log_management.models import Log from .models import Workplace, Picture, Period, TimeSlot, Reservation from .resources import (WorkplaceResource, PeriodResource, TimeSlotResource, @@ -186,13 +188,29 @@ def destroy(self, request, *args, **kwargs): "cancelation.html", merge_data ) - django_send_mail( - "Annulation d'un bloc de rédaction", - plain_msg, - settings.DEFAULT_FROM_EMAIL, - [reservation.user.email], - html_message=msg_html, - ) + + try: + django_send_mail( + "Annulation d'un bloc de rédaction", + plain_msg, + settings.DEFAULT_FROM_EMAIL, + [reservation.user.email], + html_message=msg_html, + ) + except Exception as err: + additional_data = { + 'title': "Annulation d'un bloc de rédaction", + 'default_from': settings.DEFAULT_FROM_EMAIL, + 'user_email': user.email, + 'merge_data': merge_data, + 'template': 'cancelation' + } + Log.error( + source='SENDING_BLUE_TEMPLATE', + message=err, + additional_data=json.dumps(additional_data) + ) + raise instance.time_slots.all().delete() @@ -378,13 +396,28 @@ def destroy(self, request, *args, **kwargs): "cancelation.html", merge_data ) - django_send_mail( - "Annulation d'un bloc de rédaction", - plain_msg, - settings.DEFAULT_FROM_EMAIL, - [reservation.user.email], - html_message=msg_html, - ) + try: + django_send_mail( + "Annulation d'un bloc de rédaction", + plain_msg, + settings.DEFAULT_FROM_EMAIL, + [reservation.user.email], + html_message=msg_html, + ) + except Exception as err: + additional_data = { + 'title': "Annulation d'un bloc de rédaction", + 'default_from': settings.DEFAULT_FROM_EMAIL, + 'user_email': reservation.user.email, + 'merge_data': merge_data, + 'template': 'cancelation' + } + Log.error( + source='SENDING_BLUE_TEMPLATE', + message=err, + additional_data=json.dumps(additional_data) + ) + raise return Response(status=status.HTTP_204_NO_CONTENT)