From 0148d193eea73801eac066e8deaae1b02e8395d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?c=C4=83t=C4=83lin?= Date: Mon, 25 Jan 2021 02:57:36 +0100 Subject: [PATCH] new: basic email template with csv attachments for the orded and each line --- .drone.yml | 1 + bernini/templates/order_sold.html | 19 +++--- order/migrations/0002_auto_20210124_2304.py | 53 ++++++++++------- order/models.py | 17 +++++- order/tests.py | 57 +++++++++--------- order/views.py | 58 +++++++++++------- poetry.lock | 65 +-------------------- pyproject.toml | 1 - 8 files changed, 127 insertions(+), 144 deletions(-) diff --git a/.drone.yml b/.drone.yml index 15f8740..c2c1027 100644 --- a/.drone.yml +++ b/.drone.yml @@ -10,6 +10,7 @@ steps: - apt-get update -y - apt-get -y install python3.7 python3.7-dev python curl python3.7-distutils - curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - + - cp bernini/settings.py-template bernini/settings.py - ~/.poetry/bin/poetry install - ~/.poetry/bin/poetry run python ./manage.py migrate - ~/.poetry/bin/poetry run python ./manage.py test \ No newline at end of file diff --git a/bernini/templates/order_sold.html b/bernini/templates/order_sold.html index 5294c53..4da4b82 100644 --- a/bernini/templates/order_sold.html +++ b/bernini/templates/order_sold.html @@ -1,17 +1,14 @@ - - - Order {{ order.name }} -

- Hello {{ order.sent_to.first_name }}! + Hello {{ order.sold_to.first_name }}!

- We are preparing your order {{ order.name }} as it has been confirmed recently - on the {{ order.sent_at }} - + We are preparing your order {{ order.name }} as it has been confirmed recently - on the {{ order.sold_at }} +

+

Your order detail is as follows:

@@ -26,11 +23,15 @@ {% for line in order.saleorderline_set.all %} - line.product.name - line.quantity + {{ line.product.name }} + {{ line.quantity }} {% endfor %} + + + Order {{ order.name }} + \ No newline at end of file diff --git a/order/migrations/0002_auto_20210124_2304.py b/order/migrations/0002_auto_20210124_2304.py index 911d15b..9942005 100644 --- a/order/migrations/0002_auto_20210124_2304.py +++ b/order/migrations/0002_auto_20210124_2304.py @@ -9,47 +9,60 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('order', '0001_initial'), + ("order", "0001_initial"), ] operations = [ migrations.RemoveField( - model_name='saleorder', - name='state', + model_name="saleorder", + name="state", ), migrations.AddField( - model_name='saleorder', - name='sold_at', + model_name="saleorder", + name="sold_at", field=models.DateTimeField(default=None, max_length=200, null=True), ), migrations.AddField( - model_name='saleorder', - name='sold_to', - field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL), + model_name="saleorder", + name="sold_to", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='saleorder', - name='total', + model_name="saleorder", + name="total", field=models.FloatField(default=0.0, max_length=200), ), migrations.AddField( - model_name='saleorderline', - name='order', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='order.saleorder'), + model_name="saleorderline", + name="order", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="order.saleorder", + ), ), migrations.AddField( - model_name='saleorderline', - name='quantity', + model_name="saleorderline", + name="quantity", field=models.IntegerField(default=0), ), migrations.AlterField( - model_name='saleorder', - name='name', + model_name="saleorder", + name="name", field=models.CharField(max_length=200, unique=True), ), migrations.AlterField( - model_name='saleorderline', - name='product', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='order.product'), + model_name="saleorderline", + name="product", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + to="order.product", + ), ), ] diff --git a/order/models.py b/order/models.py index 2e7e4b4..02a8cd0 100644 --- a/order/models.py +++ b/order/models.py @@ -16,7 +16,9 @@ class SaleOrder(BaseModel): readonly = ["sold_to", "sold_at", "total"] name = models.CharField(max_length=200, unique=True) - sold_to = models.ForeignKey(User, on_delete=models.DO_NOTHING, default=None, null=True) + sold_to = models.ForeignKey( + User, on_delete=models.DO_NOTHING, default=None, null=True + ) total = models.FloatField(max_length=200, default=0.0) sold_at = models.DateTimeField(max_length=200, null=True, default=None) @@ -37,6 +39,14 @@ class SaleOrder(BaseModel): self.save() return self + def as_csv(self) -> str: + lines_parsed = "" + for line in self.saleorderline_set.all(): + lines_parsed = f"{lines_parsed}::{line.id}/{line.name}" + return f"""name,sold_to,total,sold_at,lines +{self.name},{self.sold_to.first_name} {self.sold_to.last_name},{self.total},{self.sold_at},{lines_parsed} + """ + class Product(models.Model): name = models.CharField(max_length=200) @@ -48,3 +58,8 @@ class SaleOrderLine(models.Model): product = models.ForeignKey(Product, on_delete=models.DO_NOTHING, null=True) order = models.ForeignKey(SaleOrder, on_delete=models.CASCADE, null=True) quantity = models.IntegerField(default=1) + + def as_csv(self) -> str: + return f"""name,product,quantity +{self.name},{self.product.name},{self.quantity} + """ diff --git a/order/tests.py b/order/tests.py index 482fba9..b160642 100644 --- a/order/tests.py +++ b/order/tests.py @@ -9,10 +9,18 @@ from order.serializers import SaleOrderSerializer class OrderTests(TestCase): def setUp(self) -> None: - self.product_1: Product = Product.objects.create(name="Product 1", unit_price=10.0) - self.product_2: Product = Product.objects.create(name="Product 2", unit_price=20.0) - self.product_3: Product = Product.objects.create(name="Product 3", unit_price=30.0) - self.product_4: Product = Product.objects.create(name="Product 4", unit_price=40.0) + self.product_1: Product = Product.objects.create( + name="Product 1", unit_price=10.0 + ) + self.product_2: Product = Product.objects.create( + name="Product 2", unit_price=20.0 + ) + self.product_3: Product = Product.objects.create( + name="Product 3", unit_price=30.0 + ) + self.product_4: Product = Product.objects.create( + name="Product 4", unit_price=40.0 + ) self.order_1: SaleOrder = SaleOrder.objects.create(name="Order 1") self.order_2: SaleOrder = SaleOrder.objects.create(name="Order 2") @@ -21,7 +29,10 @@ class OrderTests(TestCase): name="Line 1/Order1", order=self.order_1, product=self.product_1 ) self.line_1_2: SaleOrderLine = SaleOrderLine.objects.create( - name="Line 2/Order 1", quantity=3, order=self.order_1, product=self.product_2 + name="Line 2/Order 1", + quantity=3, + order=self.order_1, + product=self.product_2, ) self.line_2_1: SaleOrderLine = SaleOrderLine.objects.create( @@ -32,14 +43,10 @@ class OrderTests(TestCase): name="Line 2/Order2", order=self.order_2, product=self.product_4 ) - self.user_foo: User = User.objects.create( - username="foo" - ) + self.user_foo: User = User.objects.create(username="foo") self.user_foo.set_password("foo") self.user_foo.save() - self.user_bar = User.objects.create( - username="bar", password="bar" - ) + self.user_bar = User.objects.create(username="bar", password="bar") self.client = Client() self.client.login(username="foo", password="foo") @@ -73,35 +80,25 @@ class OrderTests(TestCase): def test_view_readonly(self): response = self.client.patch( path="/api/orders/1/", - data={ - "sold_at": "foo" - }, + data={"sold_at": "foo"}, content_type="application/json", ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) response = self.client.patch( path="/api/orders/1/", - data={ - "sold_to": self.user_foo.id - }, - content_type="application/json" + data={"sold_to": self.user_foo.id}, + content_type="application/json", ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_view_total(self): - response = self.client.get( - path="/api/orders/1/total/" - ) - self.assertEqual(json.loads(response.content.decode())['amount_total'], 70.0) - response = self.client.get( - path="/api/orders/2/total/" - ) - self.assertEqual(json.loads(response.content.decode())['amount_total'], 70.0) + response = self.client.get(path="/api/orders/1/total/") + self.assertEqual(json.loads(response.content.decode())["amount_total"], 70.0) + response = self.client.get(path="/api/orders/2/total/") + self.assertEqual(json.loads(response.content.decode())["amount_total"], 70.0) def test_view_sell(self): - response = self.client.get( - path="/api/orders/2/sold/" - ) + response = self.client.get(path="/api/orders/2/sold/") self.order_2.refresh_from_db() - self.assertEqual(self.order_2.sold_to, self.user_foo) \ No newline at end of file + self.assertEqual(self.order_2.sold_to, self.user_foo) diff --git a/order/views.py b/order/views.py index e81913b..63f433e 100644 --- a/order/views.py +++ b/order/views.py @@ -1,9 +1,9 @@ -from django.core.mail import EmailMessage +from django.core.mail import EmailMessage, EmailMultiAlternatives from django.shortcuts import render +from django.template.loader import render_to_string from rest_framework import viewsets, permissions, status from rest_framework.decorators import action from rest_framework.response import Response - from bernini import settings from order.models import SaleOrder, SaleOrderLine, Product from order.serializers import ( @@ -33,7 +33,7 @@ class SaleOrderViewSet(viewsets.ModelViewSet): if request.data.get(field) != SaleOrderSerializer(order).data[field]: return Response( data=f"You can't manually set the field {field}", - status=status.HTTP_400_BAD_REQUEST + status=status.HTTP_400_BAD_REQUEST, ) return super(SaleOrderViewSet, self).update(request, *args, **kwargs) @@ -44,7 +44,7 @@ class SaleOrderViewSet(viewsets.ModelViewSet): if request.data.get(field) != SaleOrderSerializer(order).data[field]: return Response( data=f"You can't manually set the field {field}", - status=status.HTTP_400_BAD_REQUEST + status=status.HTTP_400_BAD_REQUEST, ) return super(SaleOrderViewSet, self).partial_update(request, *args, **kwargs) @@ -73,30 +73,46 @@ class SaleOrderViewSet(viewsets.ModelViewSet): @action(detail=True) def sent(self, request, pk=None): order: SaleOrder = self.get_object() - if order.sold_at and order.sold_to.email and settings.EMAIL_HOST_USER: - mail = EmailMessage( - f'Order {order.name} from Zapatos Bernini', - render( - 'order_sold.html', - { - 'order': order - }, - ), - settings.EMAIL_HOST_USER, - [order.sold_to.email], - reply_to=[settings.EMAIL_HOST_USER] + mail = EmailMultiAlternatives( + subject=f"Order {order.name} from Zapatos Bernini", + from_email=settings.EMAIL_HOST_USER, + to=[order.sold_to.email], + reply_to=[settings.EMAIL_HOST_USER], ) + mail.attach_alternative( + render_to_string( + template_name="order_sold.html", + context={"order": order}, + ), + mimetype="text/html", + ) + mail.attach( + filename=f"Bernini order {order.name}.csv", + content=order.as_csv(), + mimetype="text/csv", + ) + for line in order.saleorderline_set.all(): + mail.attach( + filename=f"Bernini order {order.name}/{line.name}.csv", + content=line.as_csv(), + mimetype="text/csv", + ) mail.send() - return Response(status=status.HTTP_200_OK, - data=f"Email sent! You should receive it at {order.sold_to.email}") - return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR, data="Something went wrong on our end") + return Response( + status=status.HTTP_200_OK, + data=f"Email sent! You should receive it at {order.sold_to.email}", + ) + return Response( + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + data="Something went wrong on our end", + ) class SaleOrderLineViewSet(viewsets.ModelViewSet): """ - API endpoint that allows orders to be viewed or edited. - """ + API endpoint that allows orders to be viewed or edited. + """ queryset = SaleOrderLine.objects.all().order_by("name") serializer_class = SaleOrderLineSerializer diff --git a/poetry.lock b/poetry.lock index 1d1159b..95fa0bf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -64,17 +64,6 @@ asgiref = ">=3.2.10,<4" argon2 = ["argon2-cffi (>=16.1.0)"] bcrypt = ["bcrypt"] -[[package]] -name = "django-lint" -version = "2.0.4" -description = "Static analysis tool for Django projects." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -pylint = "<1.0" - [[package]] name = "djangorestframework" version = "3.12.2" @@ -86,34 +75,11 @@ python-versions = ">=3.5" [package.dependencies] django = ">=2.2" -[[package]] -name = "logilab-astng" -version = "0.24.3" -description = "rebuild a new abstract syntax tree from Python's ast" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -logilab-common = ">=0.53.0" - -[[package]] -name = "logilab-common" -version = "1.8.1" -description = "collection of low-level Python packages and modules used by Logilab projects" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -mypy-extensions = "*" -typing-extensions = "*" - [[package]] name = "mypy-extensions" version = "0.4.3" description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "main" +category = "dev" optional = false python-versions = "*" @@ -133,18 +99,6 @@ category = "main" optional = false python-versions = ">=3.5" -[[package]] -name = "pylint" -version = "0.28.0" -description = "python code static checker" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -logilab-astng = ">=0.24.3" -logilab-common = ">=0.53.0" - [[package]] name = "pytz" version = "2020.5" @@ -197,7 +151,7 @@ python-versions = "*" name = "typing-extensions" version = "3.7.4.3" description = "Backported and Experimental Type Hints for Python 3.5+" -category = "main" +category = "dev" optional = false python-versions = "*" @@ -212,7 +166,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "aaf6dbea36a6c1cea3e3e54eaea7e189f818a37eabcb0362b93307b790d2a634" +content-hash = "77ab632e41ccac8c1f660cfa95a2286225dc42b526eae2f5500f3d2d000a5f92" [metadata.files] appdirs = [ @@ -234,20 +188,10 @@ django = [ {file = "Django-3.1.5-py3-none-any.whl", hash = "sha256:efa2ab96b33b20c2182db93147a0c3cd7769d418926f9e9f140a60dca7c64ca9"}, {file = "Django-3.1.5.tar.gz", hash = "sha256:2d78425ba74c7a1a74b196058b261b9733a8570782f4e2828974777ccca7edf7"}, ] -django-lint = [ - {file = "django-lint-2.0.4.tar.gz", hash = "sha256:25f28091d40f4586aa46cd3cab3d98665067ade95d7a45013ab24f03bb9989f1"}, -] djangorestframework = [ {file = "djangorestframework-3.12.2-py3-none-any.whl", hash = "sha256:0209bafcb7b5010fdfec784034f059d512256424de2a0f084cb82b096d6dd6a7"}, {file = "djangorestframework-3.12.2.tar.gz", hash = "sha256:0898182b4737a7b584a2c73735d89816343369f259fea932d90dc78e35d8ac33"}, ] -logilab-astng = [ - {file = "logilab-astng-0.24.3.tar.gz", hash = "sha256:e08fba39689e5a4dfa175749874811e516f019656544874defe05154073f69d4"}, -] -logilab-common = [ - {file = "logilab-common-1.8.1.tar.gz", hash = "sha256:4a50659f6f952af58654f89f65136214025ca203406f3508642d5c2e1c83d30c"}, - {file = "logilab_common-1.8.1-py3-none-any.whl", hash = "sha256:3f150dd8d8a67d8965032c6fcfb2a31adeaf587e0fe15416c5f6598e03cae844"}, -] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, @@ -260,9 +204,6 @@ pygments = [ {file = "Pygments-2.7.4-py3-none-any.whl", hash = "sha256:bc9591213a8f0e0ca1a5e68a479b4887fdc3e75d0774e5c71c31920c427de435"}, {file = "Pygments-2.7.4.tar.gz", hash = "sha256:df49d09b498e83c1a73128295860250b0b7edd4c723a32e9bc0d295c7c2ec337"}, ] -pylint = [ - {file = "pylint-0.28.0.tar.gz", hash = "sha256:310a03c37148e53521cb4ab704bdba0a2a59af091261c89dde3239320f8fa458"}, -] pytz = [ {file = "pytz-2020.5-py2.py3-none-any.whl", hash = "sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4"}, {file = "pytz-2020.5.tar.gz", hash = "sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5"}, diff --git a/pyproject.toml b/pyproject.toml index a74c7bb..6fd711a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ Pygments = "^2.7.4" PyYAML = "^5.4.1" uritemplate = "^3.0.1" - [tool.poetry.dev-dependencies] black = "^20.8b1"