diff --git a/DjangoFiles/ApiBillet/static/ticket/barlowcondensed-bold.otf b/DjangoFiles/ApiBillet/static/ticket/barlowcondensed-bold.otf new file mode 100644 index 0000000..514fc07 Binary files /dev/null and b/DjangoFiles/ApiBillet/static/ticket/barlowcondensed-bold.otf differ diff --git a/DjangoFiles/ApiBillet/static/ticket/barlowcondensed-light.otf b/DjangoFiles/ApiBillet/static/ticket/barlowcondensed-light.otf new file mode 100644 index 0000000..1a261b5 Binary files /dev/null and b/DjangoFiles/ApiBillet/static/ticket/barlowcondensed-light.otf differ diff --git a/DjangoFiles/ApiBillet/static/ticket/barlowcondensed-regular.otf b/DjangoFiles/ApiBillet/static/ticket/barlowcondensed-regular.otf new file mode 100644 index 0000000..d8932c8 Binary files /dev/null and b/DjangoFiles/ApiBillet/static/ticket/barlowcondensed-regular.otf differ diff --git a/DjangoFiles/ApiBillet/static/ticket/librebarcode128-regular.ttf b/DjangoFiles/ApiBillet/static/ticket/librebarcode128-regular.ttf new file mode 100644 index 0000000..15ff2ba Binary files /dev/null and b/DjangoFiles/ApiBillet/static/ticket/librebarcode128-regular.ttf differ diff --git a/DjangoFiles/ApiBillet/static/ticket/ticket.css b/DjangoFiles/ApiBillet/static/ticket/ticket.css new file mode 100644 index 0000000..e4b2fa7 --- /dev/null +++ b/DjangoFiles/ApiBillet/static/ticket/ticket.css @@ -0,0 +1,149 @@ +@font-face { + font-family: Libre Barcode; + src: url(librebarcode128-regular.ttf); +} +@font-face { + font-family: Barlow Condensed; + src: url(barlowcondensed-regular.otf); +} +@font-face { + font-family: Barlow Condensed; + font-weight: 300; + src: url(barlowcondensed-light.otf); +} +@font-face { + font-family: Barlow Condensed; + font-weight: 700; + src: url(barlowcondensed-bold.otf); +} + +@page { + margin: 0; + size: landscape; +} + +html { + align-content: center; + align-items: center; + background: #eef1f5; + display: flex; + font-family: Barlow Condensed, sans-serif; + height: 100%; + justify-content: center; +} +body { + background: #fff; + box-sizing: border-box; + color: #2A3239; + display: flex; + flex-wrap: wrap; + height: 8cm; + justify-content: space-between; + margin: 0; + width: 25cm; +} + +section { + box-sizing: border-box; +} + +dl { + columns: 4; + text-align: center; +} +dt { + font-size: 9pt; + font-weight: 700; + text-transform: uppercase; +} +dd { + margin-left: 0; +} +ul { + align-items: center; + display: flex; + list-style: none; + margin: 0; + padding-left: 0; +} +li { + font-weight: 700; + text-transform: uppercase; +} + +#informations { + flex: 1; + padding: 0; + position: relative; +} +#informations h1 { + display: inline-block; + font-size: 25pt; + font-weight: 300; + text-transform: uppercase; +} +#informations #name { + margin-left: 1cm; +} +#informations #destination { + position: absolute; + right: 1cm; +} +#informations dl { + background: #2A3239; + color: #fff; + margin: 0; + padding: 1cm 0; +} +#informations dd { + border-left: 1pt solid #fff; + font-size: 35pt; +} +#informations dd:first-of-type { + border-left: 0; +} +#informations ul { + margin-left: 1cm; +} +#informations li { + font-weight: 300; + padding: 0.15cm; +} +#informations li:first-of-type { + background: #2A3239; + border-radius: 4pt; + color: #fff; +} +#informations li:last-of-type { + font-family: Libre Barcode, cursive; + font-size: 25pt; + margin-left: auto; + padding-right: 1cm; + padding-top: 0.5cm; +} + +#ticket { + border-left: 1pt dashed #2A3239; + display: flex; + flex-direction: column; + height: 8cm; + justify-content: space-around; + padding: 0 1cm; +} +#ticket h2 { + font-weight: 300; + margin: 0; + text-transform: uppercase; +} +#ticket p { + font-family: Libre Barcode, cursive; + font-size: 25pt; + margin: 0; + text-align: center; +} +#ticket dl { + margin: 0; +} +#ticket li { + margin: 0 0.25cm; +} diff --git a/DjangoFiles/ApiBillet/templates/mails/buy_confirmation.html b/DjangoFiles/ApiBillet/templates/mails/buy_confirmation.html new file mode 100644 index 0000000..f2f2e37 --- /dev/null +++ b/DjangoFiles/ApiBillet/templates/mails/buy_confirmation.html @@ -0,0 +1,611 @@ + + + + + + + + + + Vos billets + + + + + + + +
+
+ Vos billets pour {{ config.organisation }} +
+ +
+  ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌        +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + +
+ + + + +
+ + + + + + +
+ + Logo + +
+
+
+
+
+ + + + +
+ + + + +
+ + + + +
+ + + + + + + + + + + + + + + + + + + +
+ {{ config.organisation }} +
+ Bonjour ! +

+ Nous vous confirmons votre achat + d'un total de {{ reservation.total_paid | floatformat:2 }}€. + + Grand merci pour votre reservation ! +

+ {% if reservation.tickets %} + Vous trouverez vos tickets en pièce jointe. + {% else %} + Aucun ticket pour un concert ? Etrange etrange + {% endif %} +
+ + + + +
+ + + + +
+ + TELECHARGER REÇU +
+
+
+
+
+
+
+ + + + +
+ + + + +
+ + + + +
+ + + + + + + + + + + + + + + +
+ Vous entrez maintenant dans le réseau TiBillet ! +
+ + TiBillet est une + solution coopérative et open-source de gestion associative, + de cashless et de billetterie orientée économie + sociale et solidaire. +

+ Vous pouvez acheter des billets dans tout le reseau avec + votre + carte cashless, recharger cette dernière en ligne ou sur + place, + et en profiter dans tout les lieux partenaires ! +

+ Vous êtes artiste ? organisateur ? Vous pouvez créer votre + espace + TiBillet en quelques clics et démarcher dans tout le reseau + coopératif ! +

+ Si vous voulez en savoir plus, n'hésitez pas à visiter notre + wiki ! +

+ +
+
+
+
+
+ +
+                       +                       +                 +
+ + diff --git a/DjangoFiles/ApiBillet/templates/ticket/ticket.html b/DjangoFiles/ApiBillet/templates/ticket/ticket.html new file mode 100644 index 0000000..2bfd769 --- /dev/null +++ b/DjangoFiles/ApiBillet/templates/ticket/ticket.html @@ -0,0 +1,53 @@ + + + {% load static %} + + + + + Boarding ticket + + + + +
+

Théodore Marcelin

+

CDG ✈ LFLL

+
+
Flight
+
DL31
+
Gate
+
29
+
Seat
+
26E
+
Zone
+
4
+
+ +
+ +
+

1257797706706

+

Théodore Marcelin

+
+
Flight
+
DL31
+
Gate
+
29
+
Seat
+
26E
+
Zone
+
4
+
+ +
+ + diff --git a/DjangoFiles/ApiBillet/templates/ticket/ticket.pdf b/DjangoFiles/ApiBillet/templates/ticket/ticket.pdf new file mode 100644 index 0000000..a3b3f0f Binary files /dev/null and b/DjangoFiles/ApiBillet/templates/ticket/ticket.pdf differ diff --git a/DjangoFiles/ApiBillet/thread_mailer.py b/DjangoFiles/ApiBillet/thread_mailer.py new file mode 100644 index 0000000..a23699f --- /dev/null +++ b/DjangoFiles/ApiBillet/thread_mailer.py @@ -0,0 +1,98 @@ +import os +import threading +from django.core.mail import send_mail, EmailMessage, EmailMultiAlternatives +from django.template.loader import render_to_string +from django.utils import timezone +from weasyprint import HTML +from BaseBillet.models import Configuration, Reservation, Ticket + +import logging +logger = logging.getLogger(__name__) + + +''' +from ApiBillet.thread_mailer import ThreadMaileur +config = Configuration.get_solo() +context = {'config': config, } +mail = ThreadMaileur('jturbeaux@pm.me', "Vos Billets", template='mails/ticket.html', context=context) +mail.send_with_tread() +''' + + +class ThreadMaileur(): + + def __init__(self, email, title, text=None, html=None, template=None, context=None): + self.title = title + self.email = email + self.text = text + self.html = html + self.config = Configuration.get_solo() + self.context = None + if template and context : + self.html = render_to_string(template, context=context) + self.context = context + self.attached_file = self._attached_file() + + def _attached_file(self): + attached_file = [] + if self.context : + if self.context.get('reservation'): + reservation: Reservation = self.context.get('reservation') + tickets = reservation.tickets.filter(status=Ticket.NOT_SCANNED) + if len(tickets) > 0: + for ticket in tickets : + attached_file.append(render_to_string('ticket/ticket.html', context={'context': 'context'})) + return attached_file + + def config_valid(self): + EMAIL_HOST = os.environ.get('EMAIL_HOST') + EMAIL_PORT = os.environ.get('EMAIL_PORT') + EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER') + EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD') + + if EMAIL_HOST and EMAIL_PORT and EMAIL_HOST_USER and EMAIL_HOST_PASSWORD and self.config.email : + return True + else: + return False + + def send(self): + if self.html and self.config_valid() : + logger.info(f' send_mail') + mail = EmailMultiAlternatives( + self.title, + self.text, + self.config.email, + [self.email,], + ) + mail.attach_alternative(self.html, "text/html") + + # msg = EmailMessage(subject, html_content, from_email, [to]) + # msg.content_subtype = "html" # Main content is now text/html + # msg.send() + + # import ipdb; ipdb.set_trace() + i=1 + for file in self.attached_file: + html_before_pdf = HTML(string=file) + mail.attach(f'ticket_{i}.pdf', html_before_pdf.write_pdf(), 'application/pdf') + i += 1 + + + mail_return = mail.send(fail_silently=False) + if mail_return == 1 : + logger.info(f' mail envoyé : {mail_return} - {self.email}') + else : + logger.error(f' mail non envoyé : {mail_return} - {self.email}') + return mail + else : + logger.error(f'Pas de contenu HTML ou de configuration email valide') + raise ValueError('Pas de contenu HTML ou de configuration email valide') + + + def send_with_tread(self): + + self.send() + # logger.info(f'{timezone.now()} on lance le thread email {self.email}') + # thread_email = threading.Thread(target=self.send) + # thread_email.start() + # logger.info(f'{timezone.now()} Thread email lancé') \ No newline at end of file diff --git a/DjangoFiles/ApiBillet/urls.py b/DjangoFiles/ApiBillet/urls.py index bf5d0ac..1890724 100644 --- a/DjangoFiles/ApiBillet/urls.py +++ b/DjangoFiles/ApiBillet/urls.py @@ -4,6 +4,8 @@ from django.urls import include, path, re_path from ApiBillet import views as api_view from rest_framework import routers +from ApiBillet.views import TicketPdf + router = routers.DefaultRouter() router.register(r'events', api_view.EventsViewSet, basename='event') router.register(r'products', api_view.ProductViewSet, basename='product') @@ -13,4 +15,6 @@ router.register(r'reservations', api_view.ReservationViewset, basename='reservat urlpatterns = [ path('', include(router.urls)), + path('ticket/', TicketPdf.as_view()), + ] \ No newline at end of file diff --git a/DjangoFiles/ApiBillet/views.py b/DjangoFiles/ApiBillet/views.py index d567ddb..c0e3008 100644 --- a/DjangoFiles/ApiBillet/views.py +++ b/DjangoFiles/ApiBillet/views.py @@ -1,17 +1,25 @@ +from datetime import datetime + +from django.http import Http404 from django.shortcuts import render # Create your views here. +from django.utils import timezone +from django_weasyprint import WeasyTemplateView from rest_framework.generics import get_object_or_404 +from rest_framework.permissions import AllowAny from rest_framework.response import Response from ApiBillet.serializers import EventSerializer, PriceSerializer, ProductSerializer, ReservationSerializer, \ ReservationValidator from AuthBillet.models import TenantAdminPermission from Customers.models import Client, Domain -from BaseBillet.models import Event, Price, Product, Reservation +from BaseBillet.models import Event, Price, Product, Reservation, Configuration, Ticket from rest_framework import viewsets, permissions, status import os +import logging +logger = logging.getLogger(__name__) def new_tenants(schema_name): @@ -130,7 +138,28 @@ class ReservationViewset(viewsets.ViewSet): return Response(validator.data, status=status.HTTP_201_CREATED) return Response(validator.errors, status=status.HTTP_400_BAD_REQUEST) - def get_permissions(self): permission_classes = [TenantAdminPermission] - return [permission() for permission in permission_classes] \ No newline at end of file + return [permission() for permission in permission_classes] + + +class TicketPdf(WeasyTemplateView): + permission_classes = [AllowAny] + template_name = 'ticket/ticket.html' + + def get_context_data(self, pk_uuid, **kwargs): + logger.info(f"{timezone.now()} création de pdf demandé. uuid : {pk_uuid}") + self.config = Configuration.get_solo() + ticket: Ticket = get_object_or_404(Ticket, uuid=pk_uuid) + kwargs['ticket'] = ticket + kwargs['config'] = self.config + + self.nom_prenom = f"{ticket.first_name.upper()}_{ticket.last_name.capitalize()}" + + return kwargs + + def get_pdf_filename(self, **kwargs): + nom_prenom = self.nom_prenom + return f"Ticket_{nom_prenom}.pdf" + +# diff --git a/DjangoFiles/BaseBillet/models.py b/DjangoFiles/BaseBillet/models.py index 6a28708..022bf53 100644 --- a/DjangoFiles/BaseBillet/models.py +++ b/DjangoFiles/BaseBillet/models.py @@ -3,6 +3,7 @@ import uuid import requests from django.contrib.auth import get_user_model from django.db import models +from django.db.models.aggregates import Sum # Create your models here. from django.db.models import Q @@ -314,8 +315,6 @@ class Event(models.Model): verbose_name_plural = _('Evenements') - - class Reservation(models.Model): uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True, db_index=True) datetime = models.DateTimeField(auto_now=True) @@ -337,6 +336,7 @@ class Reservation(models.Model): status = models.CharField(max_length=3, choices=TYPE_CHOICES, default=UNPAID, verbose_name=_("Status de la réservation")) + mail_send = models.BooleanField(default=False) # paiement = models.OneToOneField(Paiement_stripe, on_delete=models.PROTECT, blank=True, null=True, # related_name='reservation') @@ -348,10 +348,30 @@ class Reservation(models.Model): def user_mail(self): return self.user_commande.email + def paiements_paid(self): + return self.paiements.filter( + Q(status=Paiement_stripe.PAID) | Q(status=Paiement_stripe.VALID) + ) + + def articles_paid(self): + articles_paid = [] + for paiement in self.paiements_paid(): + for ligne in paiement.lignearticle_set.filter( + Q(status=LigneArticle.PAID) | Q(status=LigneArticle.VALID) + ): + articles_paid.append(ligne) + return articles_paid + + def total_paid(self): + total_paid = 0 + for article in self.articles_paid(): + article: LigneArticle + total_paid += article.price.prix * article.qty + return total_paid + def __str__(self): return f"{str(self.uuid).partition('-')[0]} - {self.user_commande.email}" - # def total_billet(self): # total = 0 # for ligne in self.paiements.all(): @@ -372,8 +392,6 @@ class Reservation(models.Model): # - - class Ticket(models.Model): uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True, db_index=True) @@ -410,7 +428,6 @@ class Ticket(models.Model): ordering = ('-datetime',) - class Paiement_stripe(models.Model): """ La commande @@ -437,7 +454,8 @@ class Paiement_stripe(models.Model): (CANCELED, 'Annulée'), ) - reservation = models.ForeignKey(Reservation, on_delete=models.PROTECT, blank=True, null=True) + reservation = models.ForeignKey(Reservation, on_delete=models.PROTECT, blank=True, null=True, + related_name="paiements") status = models.CharField(max_length=1, choices=STATUT_CHOICES, default=NON, verbose_name="Statut de la commande") @@ -446,7 +464,8 @@ class Paiement_stripe(models.Model): (QRCODE, _('Depuis scan QR-Code')), (API_BILLETTERIE, _('Depuis billetterie')), ) - source = models.CharField(max_length=1, choices=SOURCE_CHOICES, default=API_BILLETTERIE, verbose_name="Source de la commande") + source = models.CharField(max_length=1, choices=SOURCE_CHOICES, default=API_BILLETTERIE, + verbose_name="Source de la commande") total = models.FloatField(default=0) @@ -461,8 +480,6 @@ class Paiement_stripe(models.Model): [f"{ligne.product.name} {ligne.qty * ligne.product.prix}€" for ligne in self.lignearticle_set.all()]) - - class LigneArticle(models.Model): uuid = models.UUIDField(primary_key=True, db_index=True, default=uuid.uuid4) datetime = models.DateTimeField(auto_now=True) @@ -494,4 +511,3 @@ class LigneArticle(models.Model): return self.paiement_stripe.status else: return _('no stripe send') - diff --git a/DjangoFiles/BaseBillet/signals.py b/DjangoFiles/BaseBillet/signals.py index bb3646d..f9409fd 100644 --- a/DjangoFiles/BaseBillet/signals.py +++ b/DjangoFiles/BaseBillet/signals.py @@ -1,7 +1,11 @@ import requests +from django.db import connection +from django.db.models import Q from django.db.models.signals import post_save, pre_save from django.dispatch import receiver +from django.utils import timezone +from ApiBillet.thread_mailer import ThreadMaileur from BaseBillet.models import Reservation, LigneArticle, Ticket, Product, Configuration, Paiement_stripe import logging @@ -20,21 +24,6 @@ def trigger_reservation(sender, instance: Reservation, created, **kwargs): ticket.save() -@receiver(pre_save, sender=LigneArticle) -def trigger_LigneArticle(sender, instance: LigneArticle, update_fields=None, **kwargs): - # if not created - if not instance._state.adding: - old_instance = sender.objects.get(pk=instance.pk) - new_instance = pre_save_signal_status(old_instance, instance) - - -@receiver(pre_save, sender=Paiement_stripe) -def trigger_paiement_stripe(sender, instance: Paiement_stripe, update_fields=None, **kwargs): - # if not create - if not instance._state.adding: - old_instance = sender.objects.get(pk=instance.pk) - new_instance = pre_save_signal_status(old_instance, instance) - ######################################################################## ######################## SIGNAL PRE & POST SAVE ######################## @@ -136,6 +125,8 @@ def expire_paiement_stripe(old_instance, new_instance): def valide_stripe_paiement(old_instance, new_instance): logger.info(f" TRIGGER PAIEMENT STRIPE valide_stripe_paiement {old_instance.status} to {new_instance.status}") + + pass @@ -143,8 +134,28 @@ def valide_stripe_paiement(old_instance, new_instance): def send_billet_to_mail(old_instance, new_instance): - logger.info(f" TRIGGER RESERVATION send_billet_to_mail {old_instance.status} to {new_instance.status}") - pass + if not new_instance.mail_send : + logger.info(f" TRIGGER RESERVATION send_billet_to_mail {old_instance.status} to {new_instance.status}") + new_instance : Reservation + config = Configuration.get_solo() + + if new_instance.user_commande.email: + try: + mail = ThreadMaileur( + new_instance.user_commande.email, + f"Votre reservation pour {config.organisation}", + template='mails/buy_confirmation.html', + context={ + 'config': config, + 'reservation': new_instance, + }, + ) + # import ipdb; ipdb.set_trace() + mail.send_with_tread() + except Exception as e : + logger.error(f"{timezone.now()} Erreur envoie de mail pour reservation {new_instance} : {e}") + else : + logger.info(f" TRIGGER RESERVATION mail déja envoyé {new_instance} : {new_instance.mail_send} - status : {old_instance.status} to {new_instance.status}") ######################## MOTEUR TRIGGER ######################## @@ -165,7 +176,8 @@ TRANSITIONS = { Reservation.PAID: send_billet_to_mail }, Reservation.PAID: { - LigneArticle.PAID: send_billet_to_mail, + Reservation.VALID: send_billet_to_mail, + Reservation.PAID: send_billet_to_mail, '_else_': error_regression, }, Reservation.VALID: { @@ -202,21 +214,26 @@ TRANSITIONS = { }, } +@receiver(pre_save) +def pre_save_signal_status(sender, instance, **kwargs): + # if not create + if not instance._state.adding: + sender_str = sender.__name__.upper() + dict_transition = TRANSITIONS.get(sender_str) + if dict_transition: + old_instance = sender.objects.get(pk=instance.pk) + new_instance = instance -def pre_save_signal_status(old_instance, new_instance): - sender_str = old_instance.__class__.__name__.upper() - dict_transition = TRANSITIONS.get(sender_str) - if dict_transition: - logger.info(f"dict_transition {sender_str} {new_instance} : {old_instance.status} to {new_instance.status}") - transitions = dict_transition.get(old_instance.status, None) - if transitions: - # Par ordre de préférence : - trigger_function = transitions.get('_all_', ( - transitions.get(new_instance.status, ( - transitions.get('_else_', None) - )))) + logger.info(f"dict_transition {sender_str} {new_instance} : {old_instance.status} to {new_instance.status}") + transitions = dict_transition.get(old_instance.status, None) + if transitions: + # Par ordre de préférence : + trigger_function = transitions.get('_all_', ( + transitions.get(new_instance.status, ( + transitions.get('_else_', None) + )))) - if trigger_function: - if not callable(trigger_function): - raise Exception(f'Fonction {trigger_function} is not callable. Disdonc !?') - trigger_function(old_instance, new_instance) + if trigger_function: + if not callable(trigger_function): + raise Exception(f'Fonction {trigger_function} is not callable. Disdonc !?') + trigger_function(old_instance, new_instance) diff --git a/DjangoFiles/PaiementStripe/views.py b/DjangoFiles/PaiementStripe/views.py index d7926fa..0f649f2 100644 --- a/DjangoFiles/PaiementStripe/views.py +++ b/DjangoFiles/PaiementStripe/views.py @@ -229,11 +229,12 @@ class retour_stripe(View): return HttpResponseRedirect(f"/qr/{ligne_article.carte.uuid}#erreurpaiement") elif paiement_stripe.source == Paiement_stripe.API_BILLETTERIE : - return HttpResponse( - 'Coucou') + if paiement_stripe.status == Paiement_stripe.VALID : + return HttpResponse( + 'Coucou') - else : - raise Http404('paiement_stripe.source ?') + + raise Http404(f'{paiement_stripe.status}') ''' diff --git a/Docker/Dockerfile/dockerfile b/Docker/Dockerfile/dockerfile index 512b22b..4a122ab 100644 --- a/Docker/Dockerfile/dockerfile +++ b/Docker/Dockerfile/dockerfile @@ -61,11 +61,17 @@ RUN pip install django-stdimage RUN pip install stripe +RUN apt-get install -y fonts-font-awesome +RUN apt-get install -y libffi-dev +RUN apt-get install -y libgdk-pixbuf2.0-0 +RUN apt-get install -y libpango1.0-0 +RUN apt-get install -y python-dev +# RUN apt-get install -y python-lxml +RUN apt-get install -y shared-mime-info +RUN apt-get install -y libcairo2 +RUN pip install django-weasyprint - - +RUN apt-get -y clean RUN python --version RUN django-admin --version - -