diff --git a/artel/Dockerfile b/artel/Dockerfile index a8ed9a6..39fb199 100644 --- a/artel/Dockerfile +++ b/artel/Dockerfile @@ -22,6 +22,7 @@ RUN apt-get update --yes --quiet && apt-get install --yes --quiet --no-install-r libjpeg62-turbo-dev \ zlib1g-dev \ libwebp-dev \ + wkhtmltopdf \ && rm -rf /var/lib/apt/lists/* # Install the application server. diff --git a/artel/Dockerfile.local b/artel/Dockerfile.local index deccf97..3809fc2 100644 --- a/artel/Dockerfile.local +++ b/artel/Dockerfile.local @@ -19,6 +19,7 @@ RUN apt-get update --yes --quiet && apt-get install --yes --quiet --no-install-r libjpeg62-turbo-dev \ zlib1g-dev \ libwebp-dev \ + wkhtmltopdf \ && rm -rf /var/lib/apt/lists/* # Install the project requirements. diff --git a/artel/artel/settings/base.py b/artel/artel/settings/base.py index 977a9bb..3576ecb 100644 --- a/artel/artel/settings/base.py +++ b/artel/artel/settings/base.py @@ -170,10 +170,18 @@ WAGTAILSEARCH_BACKENDS = { # Base URL to use when referring to full URLs within the Wagtail admin backend - # e.g. in notification emails. Don't include '/admin' or a trailing slash -WAGTAILADMIN_BASE_URL = "http://example.com" +WAGTAILADMIN_BASE_URL = "https://artel.tepewu.pl" # STORE SETTINGS PRODUCTS_PER_PAGE = 6 # CART settings CART_SESSION_ID = 'cart' + +# EMAIL settings +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_HOST = os.environ.get('EMAIL_HOST', 'smtp.gmail.com') +EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER', '') +EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", '') +EMAIL_PORT = os.environ.get('EMAIL_PORT', 587) +DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'mtyton@tepewu.pl') diff --git a/artel/artel/templates/menu/custom_main_menu.html b/artel/artel/templates/menu/custom_main_menu.html index 56e8d8f..5098dcb 100644 --- a/artel/artel/templates/menu/custom_main_menu.html +++ b/artel/artel/templates/menu/custom_main_menu.html @@ -24,6 +24,3 @@
Koszyk - - - \ No newline at end of file diff --git a/artel/requirements.txt b/artel/requirements.txt index 147e103..89d53de 100644 --- a/artel/requirements.txt +++ b/artel/requirements.txt @@ -6,3 +6,5 @@ dj-database-url<=2.0.0 djangorestframework==3.14.0 phonenumbers==8.13.13 django-phonenumber-field==7.1.0 +factory-boy==3.2.1 +pdfkit==1.0.0 \ No newline at end of file diff --git a/artel/store/migrations/0005_documenttemplate_orderdocument.py b/artel/store/migrations/0005_documenttemplate_orderdocument.py new file mode 100644 index 0000000..deb0a75 --- /dev/null +++ b/artel/store/migrations/0005_documenttemplate_orderdocument.py @@ -0,0 +1,41 @@ +# Generated by Django 4.1.9 on 2023-06-04 09:38 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("store", "0004_customerdata_order_orderproduct"), + ] + + operations = [ + migrations.CreateModel( + name="DocumentTemplate", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=255)), + ("file", models.FileField(upload_to="documents")), + ( + "doc_type", + models.CharField(choices=[("agreement", "Agreement"), ("receipt", "Receipt")], max_length=255), + ), + ], + ), + migrations.CreateModel( + name="OrderDocument", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "order", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, related_name="documents", to="store.order" + ), + ), + ( + "template", + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="store.documenttemplate"), + ), + ], + ), + ] diff --git a/artel/store/models.py b/artel/store/models.py index 487f792..785da07 100644 --- a/artel/store/models.py +++ b/artel/store/models.py @@ -1,3 +1,4 @@ +import pdfkit from django.db import models from django.core.paginator import ( Paginator, @@ -5,6 +6,10 @@ from django.core.paginator import ( ) from django.conf import settings from django.core.validators import MinValueValidator +from django.template import ( + Template, + Context +) from modelcluster.models import ClusterableModel from modelcluster.fields import ParentalKey @@ -17,6 +22,10 @@ from wagtail import fields as wagtail_fields from taggit.managers import TaggableManager from phonenumber_field.modelfields import PhoneNumberField +from store.utils import ( + send_mail +) + class ProductAuthor(models.Model): name = models.CharField(max_length=255) @@ -194,8 +203,76 @@ class OrderProduct(models.Model): objects = OrderProductManager() +class OrderManager(models.Manager): + def create_from_cart(self, cart, customer_data): + order = self.create(customer=customer_data) + OrderProduct.objects.create_from_cart(cart, order) + # create proper documents + agreement_template = DocumentTemplate.objects.filter( + doc_type=DocumentTypeChoices.AGREEMENT + ).order_by("-created_at").first() + receipt_template = DocumentTemplate.objects.filter( + doc_type=DocumentTypeChoices.RECEIPT + ).order_by("-created_at").first() + agreement = OrderDocument.objects.create( + order=order, + template=agreement_template + ) + receipt = OrderDocument.objects.create( + order=order, + template=receipt_template + ) + send_mail(agreement) + send_mail(receipt) + return order + + class Order(models.Model): customer = models.ForeignKey(CustomerData, on_delete=models.CASCADE) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) sent = models.BooleanField(default=False) + + objects = OrderManager() + + @property + def order_number(self) -> str: + return f"{self.id:06}/{self.created_at.year}" + + +class DocumentTypeChoices(models.TextChoices): + AGREEMENT = "agreement" + RECEIPT = "receipt" + + +class DocumentTemplate(models.Model): + name = models.CharField(max_length=255) + file = models.FileField(upload_to="documents") + doc_type = models.CharField(max_length=255, choices=DocumentTypeChoices.choices) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.name + + +class OrderDocument(models.Model): + order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="documents") + template = models.ForeignKey(DocumentTemplate, on_delete=models.CASCADE) + sent = models.BooleanField(default=False) + + def get_document_context(self): + _context = { + "order": self.order, + "customer": self.order.customer, + "products": self.order.products.all(), + } + return Context(_context) + + @property + def document(self): + with open(self.template.file.path, "rb") as f: + content = f.read() + template = Template(content) + context = self.get_document_context() + content = template.render(context) + return pdfkit.from_string(content, False) diff --git a/artel/store/tests.py b/artel/store/tests.py deleted file mode 100644 index ee19e98..0000000 --- a/artel/store/tests.py +++ /dev/null @@ -1,36 +0,0 @@ -from django.test import TestCase -from django.urls import reverse -from store.models import ProductAuthor, ProductCategory, ProductTemplate, Product - - -# TODO - this is fine for now, but we'll want to use factoryboy for this: -# https://factoryboy.readthedocs.io/en/stable/ -# TODO - test have to rewritten - I'll do it tommorow -class CartTestCase(TestCase): - def setUp(self): - self.productid = 1 - self.author = ProductAuthor.objects.create(name='Test Author') - self.category = ProductCategory.objects.create(name='Test Category') - self.template = ProductTemplate.objects.create(category=self.category, - author=self.author, - title='Test title', - code='Test code', - description='Test description' - ) - self.product = Product.objects.create(template=self.template, - price=10.99) - self.cart_url = reverse('view_cart') - - def test_add_to_cart(self): - response = self.client.post(reverse('add_to_cart', - args=[self.productid])) - self.assertEqual(response.status_code, 302) - - def test_remove_from_cart(self): - response = self.client.post(reverse('remove_from_cart', - args=[self.productid])) - self.assertEqual(response.status_code, 302) - - def test_view_cart(self): - response = self.client.get(self.cart_url) - self.assertEqual(response.status_code, 200) diff --git a/artel/store/tests/__init__.py b/artel/store/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/artel/store/tests/factories.py b/artel/store/tests/factories.py new file mode 100644 index 0000000..0f6cb2c --- /dev/null +++ b/artel/store/tests/factories.py @@ -0,0 +1,41 @@ +from factory import ( + Faker, + SubFactory +) +from factory.django import ( + FileField, + DjangoModelFactory +) + + +class CustomerDataFactory(DjangoModelFactory): + class Meta: + model = 'store.CustomerData' + + name = Faker('name') + surname = Faker('name') + email = Faker('email') + phone = Faker('phone_number') + street = Faker('street_address') + city = Faker('city') + zip_code = Faker('postcode') + country = Faker('country') + + +class OrderFactory(DjangoModelFactory): + class Meta: + model = 'store.Order' + + customer = SubFactory(CustomerDataFactory) + created_at = Faker('date_time') + updated_at = Faker('date_time') + sent = Faker('boolean') + + +class DocumentTemplateFactory(DjangoModelFactory): + class Meta: + model = 'store.DocumentTemplate' + + name = Faker('name') + file = FileField(filename="doc.odt") + doc_type = "AGREEMENT" diff --git a/artel/store/tests/test_models.py b/artel/store/tests/test_models.py new file mode 100644 index 0000000..162facf --- /dev/null +++ b/artel/store/tests/test_models.py @@ -0,0 +1,41 @@ +from django.test import TestCase +from django.urls import reverse + +from store.tests import factories +from store import models as store_models + + +# TODO - this is fine for now, but we'll want to use factoryboy for this: +# https://factoryboy.readthedocs.io/en/stable/ +# TODO - test have to rewritten - I'll do it tommorow + +class OrderDocumentTestCase(TestCase): + def setUp(self): + super().setUp() + self.order = factories.OrderFactory() + self.document_template = factories.DocumentTemplateFactory(file__data="test") + + def test_generate_document_success(self): + order_doc = store_models.OrderDocument.objects.create( + order=self.order, + template=self.document_template + ) + document = order_doc.document + self.assertIsInstance(document, bytes) + + def test_get_document_context_success(self): + order_doc = store_models.OrderDocument.objects.create( + order=self.order, + template=self.document_template + ) + context = order_doc.get_document_context() + self.assertIsInstance(context, store_models.Context) + self.assertEqual(context["order"].id, self.order.id) + self.assertEqual(context["customer"].id, self.order.customer.id) + self.assertEqual(context["products"].count(), 0) + + def test_send_order_document_mail_success(self): + ... + + def test_send_order_document_mail_failure_wrong_email(self): + ... diff --git a/artel/store/urls.py b/artel/store/urls.py index 3dbf6d6..cfb2bb7 100644 --- a/artel/store/urls.py +++ b/artel/store/urls.py @@ -12,4 +12,5 @@ urlpatterns = [ path('cart/', store_views.CartView.as_view(), name='cart'), path("order/", store_views.OrderView.as_view(), name="order"), path("order/confirm/", store_views.OrderConfirmView.as_view(), name="order-confirm"), + path("send-mail/", store_views.SendMailView.as_view(), name="send-mail"), ] + router.urls diff --git a/artel/store/utils.py b/artel/store/utils.py new file mode 100644 index 0000000..9a9c7bd --- /dev/null +++ b/artel/store/utils.py @@ -0,0 +1,18 @@ +from django.core.mail import EmailMessage +from django.conf import settings + + +# TODO - add celery task for sending not sent earlier +def send_mail(order_doc): + order = order_doc.order + message = EmailMessage( + subject=f"Zamówienie {order.order_number}", + body="Dokumenty dla Twojego zamówienia", + from_email=settings.DEFAULT_FROM_EMAIL, + to=[order.customer.email] + ) + message.attach(f"{order.order_number}.pdf", order_doc.document, "application/pdf") + sent = bool(message.send()) + order_doc.sent = sent + order_doc.save() + return sent diff --git a/artel/store/views.py b/artel/store/views.py index fb61607..483daa7 100644 --- a/artel/store/views.py +++ b/artel/store/views.py @@ -20,7 +20,9 @@ from store.forms import CustomerDataForm from store.models import ( CustomerData, Order, - OrderProduct + OrderProduct, + OrderDocument, + DocumentTemplate ) @@ -133,12 +135,23 @@ class OrderConfirmView(View): def post(self, request): customer_data = CustomerData.objects.get(id=self.request.session["customer_data_id"]) cart = SessionCart(self.request) - order = Order.objects.create( - customer=customer_data, + order = Order.objects.create_from_cart( + cart, customer_data ) - OrderProduct.objects.create_from_cart(order, cart) - cart.clear() self.request.session.pop("customer_data_id") - # TODO - to be tested # TODO - messages - return HttpResponseRedirect(reverse("cart")) \ No newline at end of file + return HttpResponseRedirect(reverse("cart")) + + +class SendMailView(View): + def get(self, request): + from django.core import mail + from django.http import HttpResponse + from django.conf import settings + r = mail.send_mail( + subject=f"Test", + message="Dokumenty dla Twojego zamówienia", + from_email=settings.DEFAULT_FROM_EMAIL, + recipient_list=["mateusz.tyton99@gmail.com"] + ) + return HttpResponse(f"Mail sent: {r}")