From c21f4b3dacd597a15a5ec39d525df1dfe1b70376 Mon Sep 17 00:00:00 2001 From: jdlugosz963 Date: Mon, 17 Jul 2023 01:47:57 +0200 Subject: Add project. --- restaurant_orders/core/__init__.py | 0 restaurant_orders/core/admin.py | 5 + restaurant_orders/core/apps.py | 6 + restaurant_orders/core/decorators.py | 36 +++++ restaurant_orders/core/models.py | 90 +++++++++++ restaurant_orders/core/tasks.py | 51 ++++++ restaurant_orders/core/utils.py | 100 ++++++++++++ restaurant_orders/dashboard/__init__.py | 0 restaurant_orders/dashboard/admin.py | 3 + restaurant_orders/dashboard/apps.py | 6 + restaurant_orders/dashboard/consumers.py | 49 ++++++ restaurant_orders/dashboard/forms.py | 30 ++++ .../dashboard/templates/dashboard/dashboard.html | 152 ++++++++++++++++++ .../templates/dashboard/dashboard_order.html | 89 +++++++++++ restaurant_orders/dashboard/urls.py | 17 ++ restaurant_orders/dashboard/views.py | 95 +++++++++++ restaurant_orders/manage.py | 22 +++ restaurant_orders/restaurant_orders/__init__.py | 1 + restaurant_orders/restaurant_orders/asgi.py | 34 ++++ restaurant_orders/restaurant_orders/celery.py | 10 ++ restaurant_orders/restaurant_orders/consumers.py | 48 ++++++ restaurant_orders/restaurant_orders/settings.py | 177 +++++++++++++++++++++ .../restaurant_orders/templates/_pagination.html | 34 ++++ .../restaurant_orders/templates/base.html | 90 +++++++++++ .../restaurant_orders/templates/home.html | 10 ++ .../restaurant_orders/templates/login.html | 22 +++ .../templates/restaurants_choice.html | 24 +++ restaurant_orders/restaurant_orders/urls.py | 45 ++++++ restaurant_orders/restaurant_orders/wsgi.py | 16 ++ restaurant_orders/settings/__init__.py | 0 restaurant_orders/settings/admin.py | 1 + restaurant_orders/settings/apps.py | 6 + restaurant_orders/settings/forms.py | 18 +++ .../templates/settings/restaurant_settings.html | 22 +++ restaurant_orders/settings/urls.py | 9 ++ restaurant_orders/settings/views.py | 44 +++++ .../wordpress_integration/__init__.py | 0 restaurant_orders/wordpress_integration/admin.py | 1 + restaurant_orders/wordpress_integration/apps.py | 6 + restaurant_orders/wordpress_integration/urls.py | 8 + restaurant_orders/wordpress_integration/views.py | 22 +++ 41 files changed, 1399 insertions(+) create mode 100644 restaurant_orders/core/__init__.py create mode 100644 restaurant_orders/core/admin.py create mode 100644 restaurant_orders/core/apps.py create mode 100644 restaurant_orders/core/decorators.py create mode 100644 restaurant_orders/core/models.py create mode 100644 restaurant_orders/core/tasks.py create mode 100644 restaurant_orders/core/utils.py create mode 100644 restaurant_orders/dashboard/__init__.py create mode 100644 restaurant_orders/dashboard/admin.py create mode 100644 restaurant_orders/dashboard/apps.py create mode 100644 restaurant_orders/dashboard/consumers.py create mode 100644 restaurant_orders/dashboard/forms.py create mode 100644 restaurant_orders/dashboard/templates/dashboard/dashboard.html create mode 100644 restaurant_orders/dashboard/templates/dashboard/dashboard_order.html create mode 100644 restaurant_orders/dashboard/urls.py create mode 100644 restaurant_orders/dashboard/views.py create mode 100755 restaurant_orders/manage.py create mode 100644 restaurant_orders/restaurant_orders/__init__.py create mode 100644 restaurant_orders/restaurant_orders/asgi.py create mode 100644 restaurant_orders/restaurant_orders/celery.py create mode 100644 restaurant_orders/restaurant_orders/consumers.py create mode 100644 restaurant_orders/restaurant_orders/settings.py create mode 100644 restaurant_orders/restaurant_orders/templates/_pagination.html create mode 100644 restaurant_orders/restaurant_orders/templates/base.html create mode 100644 restaurant_orders/restaurant_orders/templates/home.html create mode 100644 restaurant_orders/restaurant_orders/templates/login.html create mode 100644 restaurant_orders/restaurant_orders/templates/restaurants_choice.html create mode 100644 restaurant_orders/restaurant_orders/urls.py create mode 100644 restaurant_orders/restaurant_orders/wsgi.py create mode 100644 restaurant_orders/settings/__init__.py create mode 100644 restaurant_orders/settings/admin.py create mode 100644 restaurant_orders/settings/apps.py create mode 100644 restaurant_orders/settings/forms.py create mode 100644 restaurant_orders/settings/templates/settings/restaurant_settings.html create mode 100644 restaurant_orders/settings/urls.py create mode 100644 restaurant_orders/settings/views.py create mode 100644 restaurant_orders/wordpress_integration/__init__.py create mode 100644 restaurant_orders/wordpress_integration/admin.py create mode 100644 restaurant_orders/wordpress_integration/apps.py create mode 100644 restaurant_orders/wordpress_integration/urls.py create mode 100644 restaurant_orders/wordpress_integration/views.py (limited to 'restaurant_orders') diff --git a/restaurant_orders/core/__init__.py b/restaurant_orders/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/restaurant_orders/core/admin.py b/restaurant_orders/core/admin.py new file mode 100644 index 0000000..5094530 --- /dev/null +++ b/restaurant_orders/core/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin +from core.models import Restaurant, Order + +admin.site.register(Restaurant) +admin.site.register(Order) diff --git a/restaurant_orders/core/apps.py b/restaurant_orders/core/apps.py new file mode 100644 index 0000000..8115ae6 --- /dev/null +++ b/restaurant_orders/core/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'core' diff --git a/restaurant_orders/core/decorators.py b/restaurant_orders/core/decorators.py new file mode 100644 index 0000000..d8583f8 --- /dev/null +++ b/restaurant_orders/core/decorators.py @@ -0,0 +1,36 @@ +from django.http import HttpResponse +from core.models import Restaurant + +import base64 +import hashlib +import hmac + +def get_webhook_secret_from_restaurant(pk): + try: + token = Restaurant.objects.get(pk=pk).woocommerce_webhook_secret + if token != '': + return token + except Restaurant.DoesNotExist: + pass + return None + +def compare_signatures(body, webhook_secret, request_sig): + signature = hmac.new(webhook_secret.encode(), body, hashlib.sha256).digest() + return hmac.compare_digest(request_sig.encode(), base64.b64encode(signature)) + + +def woocommerce_authentication_required(view): + def inner(request, restaurant_pk, *args, **kwargs): + webhook_secret = get_webhook_secret_from_restaurant(restaurant_pk) + request_sig = request.headers.get('x-wc-webhook-signature') + + if not webhook_secret or not request_sig: + return HttpResponse('Unauthorized') + + if compare_signatures(request.body, webhook_secret, request_sig): + return view(request, restaurant_pk, *args, **kwargs) + + response = HttpResponse('Unauthorized') + response.status_code = 403 + return response + return inner diff --git a/restaurant_orders/core/models.py b/restaurant_orders/core/models.py new file mode 100644 index 0000000..91147d2 --- /dev/null +++ b/restaurant_orders/core/models.py @@ -0,0 +1,90 @@ +from django.db import models +from django.contrib.auth.models import User +from django.shortcuts import Http404, get_object_or_404 + +from datetime import datetime + + +class Restaurant(models.Model): + name = models.CharField(max_length=50) + users = models.ManyToManyField(User) + + wordpress_url = models.URLField(max_length=50, unique=True) + woocommerce_consumer_key = models.CharField(max_length=50) + woocommerce_consumer_secret = models.CharField(max_length=50) + woocommerce_webhook_secret = models.CharField(max_length=50) + + @classmethod + def get_user_restaurants(cls, user: User): + return cls.objects.filter(users=user) + + @classmethod + def get_user_restaurant_or_404(cls, pk, user: User): + return get_object_or_404(cls, pk=pk, users=user) + + def __str__(self): + return self.name + + +class Order(models.Model): + WP_STATES = ( + ('pending', 'Oczekujace'), + ('processing', 'Przetwarzane'), + ('on-hold', 'Wstrzymane'), + ('completed', 'Zakonczone'), + ('cancelled', 'Anulowane'), + ('refunded', 'Zwrocone'), + ('failed', 'Nie powiodlo sie'), + ('trash', 'Usuniete') + ) + + wp_id = models.IntegerField(editable=False) + wp_status = models.CharField(max_length=30, choices=WP_STATES) + wp_order_key = models.CharField(max_length=50, editable=False) + date_created = models.DateField(editable=False) + date_modified = models.DateField() + line_items = models.JSONField() + billing = models.JSONField() + restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE) + can_display = models.BooleanField(default=True) + + @classmethod + def update_or_create_from_response(cls, response, restaurant_model): + try: + obj = cls.objects.update_or_create( + wp_id=response['id'], + wp_order_key=response['order_key'], + restaurant=restaurant_model, + defaults={ + 'wp_status': response['status'], + 'date_created': datetime.strptime(response['date_created'], '%Y-%m-%dT%H:%M:%S'), + 'date_modified': datetime.strptime(response['date_modified'], '%Y-%m-%dT%H:%M:%S'), + 'line_items': response['line_items'], + 'billing': response['billing'], + } + ) + return obj + except KeyError: + return None + + + @classmethod + def create_from_response_disable_view(cls, response, restaurant_model): + obj, _ = cls.update_or_create_from_response(response, restaurant_model) + obj.can_display = False + obj.save() + return obj + + @classmethod + def get_order(cls, pk, user: User): + try: + return cls.objects.get( + pk=pk, + can_display=True, + restaurant__users=user + ) + except cls.DoesNotExist: + raise Http404 + + def __str__(self): + return f'{self.wp_order_key} - {self.date_created}' diff --git a/restaurant_orders/core/tasks.py b/restaurant_orders/core/tasks.py new file mode 100644 index 0000000..f1c0192 --- /dev/null +++ b/restaurant_orders/core/tasks.py @@ -0,0 +1,51 @@ +from celery import shared_task + +from django.core import serializers + +from restaurant_orders.consumers import NotificationsConsumer + +from core.utils import Orders, SendMail, SendSms +import re + + +def send_notification(is_success, message, user_pk): + status = NotificationsConsumer.OK if is_success else NotificationsConsumer.ERROR + NotificationsConsumer.send_notifications( + user_pk, + status, + message + ) + + + +@shared_task +def create_order_and_send_notification(order, items, is_email=None, is_sms=None, user_pk=None): + order = [obj for obj in serializers.deserialize('json', order)] + if len(order) != 1: + return + order = order[0].object + + phone = order.billing.get('phone') + email = order.billing.get('email') + new_order = Orders(order.restaurant, order.billing).create_custom_order(items) + + +# if new_order is None: +# send_notification(False, +# "Niestety nie udalo sie skontaktowac z restauracja, prosze sprowbowac ponownie pozniej.", +# user_pk) +# return + + EMAIL_REGEX = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b' + + if is_sms: # TODO: Make regex for sms + sms = SendSms(new_order).send() + send_notification(*sms, user_pk) + + if is_email and re.fullmatch(EMAIL_REGEX, str(email)): + mail = SendMail(new_order).send() + send_notification(*mail, user_pk) + + + + diff --git a/restaurant_orders/core/utils.py b/restaurant_orders/core/utils.py new file mode 100644 index 0000000..d716a9c --- /dev/null +++ b/restaurant_orders/core/utils.py @@ -0,0 +1,100 @@ +import json +from woocommerce import API +from twilio.rest import Client +from twilio.base.exceptions import TwilioRestException + +from django.core.mail import send_mail +from django.conf import settings + +from core.models import Order + +class Sender(): + def __init__(self, order): + self.order = order + + def get_order_url(self): + order_id = self.order.wp_id + order_key = self.order.wp_order_key + restaurant_url = self.order.restaurant.wordpress_url + return f'{restaurant_url}/zamowienie/order-pay/{order_id}/?pay_for_order=true&key={order_key}' + + def get_message_body(self): + return f'Prosze dokonac platnosci: {self.get_order_url()}' + + def send(self) -> (bool, str): + pass + + +class SendSms(Sender): + def __init__(self, order): + account_sid = settings.TWILIO_ACCOUNT_SID + auth_token = settings.TWILIO_TOKEN + + self.client = Client(account_sid, auth_token) + self.from_ = "+17432007359" + + super().__init__(order) + + def send(self) -> (bool, str): + phone = self.order.billing.get('phone', None) + phone = "+48609155122" + if phone: + try: + message = self.client.messages.create(to=phone, + from_=self.from_, + body=self.get_message_body()) + except TwilioRestException as err: + return (False, err.msg) + else: + return (True, 'Wyslano sms') + return (False, 'Nie znaleziono numeru telefonu.') + + + +class SendMail(Sender): + def send(self) -> (bool, str): + email = self.order.billing.get('email', None) + email = 'jdlugosz963@gmail.com' + if email: # Jesli sie spierdoli to wypluje + try: + send_mail('Strona do zaplaty', self.get_message_body(), 'no-reply@reami.pl', (email, ), fail_silently=False) + except smtplib.SMTPException: + return (False, "Niestety nie udalo sie wyslac maila.") + else: + return (True, "Wyslano maila.") + return (False, "Nie znaleziono maila.") + + +class Orders: + def __init__(self, restaurant, billing): + self.restaurant = restaurant + self.billing = billing + + self.wcapi = API( + url=restaurant.wordpress_url, + consumer_key=restaurant.woocommerce_consumer_key, + consumer_secret=restaurant.woocommerce_consumer_secret, + timeout=7 + ) + + def get_custom_order_data(self, items): + return { + "payment_method": "bacs", + "payment_method_title": "Direct Bank Transfer", + "set_paid": False, + "billing": self.billing, + "shipping": self.billing, + "line_items": [ + { + "product_id": pk, + "total": total, + "quantity": 1, + } for pk, total in items + ] + } + + def create_custom_order(self, items): + data = self.get_custom_order_data(items) + response = self.wcapi.post("orders", data=data).json() + return Order.create_from_response_disable_view(response, self.restaurant) + diff --git a/restaurant_orders/dashboard/__init__.py b/restaurant_orders/dashboard/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/restaurant_orders/dashboard/admin.py b/restaurant_orders/dashboard/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/restaurant_orders/dashboard/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/restaurant_orders/dashboard/apps.py b/restaurant_orders/dashboard/apps.py new file mode 100644 index 0000000..7b1cc05 --- /dev/null +++ b/restaurant_orders/dashboard/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class DashboardConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'dashboard' diff --git a/restaurant_orders/dashboard/consumers.py b/restaurant_orders/dashboard/consumers.py new file mode 100644 index 0000000..17a5d34 --- /dev/null +++ b/restaurant_orders/dashboard/consumers.py @@ -0,0 +1,49 @@ +from channels.generic.websocket import AsyncWebsocketConsumer +from channels.exceptions import StopConsumer +import channels.layers + +from asgiref.sync import sync_to_async, async_to_sync + +from core.models import Restaurant, Order + +from django.db.models import signals +from django.dispatch import receiver +from django.core import serializers + + +class OrderConsumer(AsyncWebsocketConsumer): + async def connect(self): + restaurant_pk = self.scope['url_route']['kwargs']['restaurant_pk'] + self.restaurant = sync_to_async(Restaurant.get_user_restaurant_or_404)(restaurant_pk, self.scope['user']) + if not self.restaurant: + return + + self.room_name = str(restaurant_pk) + self.room_group_name = f'restaurant_{self.room_name}' + + # Join room group + await self.channel_layer.group_add( + self.room_group_name, + self.channel_name + ) + + await self.accept() + + async def disconnect(self, close_code): + raise StopConsumer + + async def new_order(self, event): + await self.send(event['data']) + + @staticmethod + @receiver(signals.post_save, sender=Order) + def order_observer(sender, instance, **kwargs): + if not instance.can_display: + return + + layer = channels.layers.get_channel_layer() + + async_to_sync(layer.group_send)(f'restaurant_{instance.restaurant.pk}', { + 'type': 'new.order', + 'data': serializers.serialize('json', (instance, )) + }) diff --git a/restaurant_orders/dashboard/forms.py b/restaurant_orders/dashboard/forms.py new file mode 100644 index 0000000..23937ed --- /dev/null +++ b/restaurant_orders/dashboard/forms.py @@ -0,0 +1,30 @@ +from django import forms + +from core.models import Order + + +FORM_TAILWIND_CLASSES = 'form-control block w-full px-3 py-1.5 text-base font-normal text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 rounded transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none' + +class OrderStatusForm(forms.ModelForm): + class Meta: + model = Order + fields = ('wp_status', ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['wp_status'].label = 'Przenies do:' + +class AddToBillForm(forms.Form): + send_mail = forms.BooleanField(label='Wyslij maila', initial=False, required=False) + send_sms = forms.BooleanField(label='Wyslij sms', initial=True, required=False) + + def __init__(self, pk, user, *args, **kwargs): + super().__init__(*args, **kwargs) + order = Order.get_order(pk, user) + + for item in order.line_items: + index = item['product_id'] + self.fields[index] = forms.IntegerField(required=False, label=item['name']) + + for index in self.fields.keys(): + self.fields[index].widget.attrs.update({'class': FORM_TAILWIND_CLASSES}) diff --git a/restaurant_orders/dashboard/templates/dashboard/dashboard.html b/restaurant_orders/dashboard/templates/dashboard/dashboard.html new file mode 100644 index 0000000..0d2bf11 --- /dev/null +++ b/restaurant_orders/dashboard/templates/dashboard/dashboard.html @@ -0,0 +1,152 @@ +{% extends "base.html" %} + +{% block title %}Dashboard{% endblock %} + +{% block content %} + {% include '_pagination.html' %} +
+ +
+ + + +
+ {% for object in object_list %} + + {% endfor %} +
+ + + + {% endblock %} diff --git a/restaurant_orders/dashboard/templates/dashboard/dashboard_order.html b/restaurant_orders/dashboard/templates/dashboard/dashboard_order.html new file mode 100644 index 0000000..f245843 --- /dev/null +++ b/restaurant_orders/dashboard/templates/dashboard/dashboard_order.html @@ -0,0 +1,89 @@ +{% extends "base.html" %} + +{% block title %}Order #{{order.wp_id}}{% endblock %} + +{% block content %} +
+ Wroc +

#{{ order.wp_id}} - {{ order.billing.first_name }} {{ order.billing.last_name }} - {{order.get_wp_status_display}}

+
+
+
+

Zamowienie

+ + + + + + + + + + + + + {% for item in order.line_items %} + + + + + + + + {% endfor %} + +
#ProduktCena za produktCena za produktyIlosc
{{ forloop.counter }}{{ item.name }}{{item.price}} zl{{item.subtotal}} zl{{item.quantity}}
+ +
    +
+
+
+ +
+
+

Adres dostawy

+
    +
  • + Imie i Nazwisko + {{order.billing.first_name}} {{order.billing.last_name}} +
  • +
  • + Numer stolika + {{order.billing.address_1}} +
  • +
  • + Telefon + {{order.billing.phone}} +
  • +
  • + Email + {{order.billing.email}} +
  • +
+
+
+ +
+
+

Doliczenie do zamowienia

+
+ {% csrf_token %} + {{addToBillForm}} + +
+
+
+ +
+
+

Akcje

+
+ {% csrf_token %} + {{orderStatusForm}} + +
+
+
+
+
+{% endblock %} diff --git a/restaurant_orders/dashboard/urls.py b/restaurant_orders/dashboard/urls.py new file mode 100644 index 0000000..cd67ce3 --- /dev/null +++ b/restaurant_orders/dashboard/urls.py @@ -0,0 +1,17 @@ +from django.urls import path +from dashboard.views import Home, DashboardView, DashboardOrderView, ChangeOrderStatusView, AddToBillView +from dashboard.consumers import OrderConsumer + +app_name="dashboard" + +urlpatterns = [ + path('', Home.as_view(), name="home"), + path('restaurant//', DashboardView.as_view(), name='restaurant_dashboard'), + path('restaurant/order//', DashboardOrderView.as_view(), name='order_dashboard'), + path('restaurant/order//change/status/', ChangeOrderStatusView.as_view(), name='order_status_change'), + path('restaurant/order//add_to_bill/', AddToBillView.as_view(), name='order_add_to_bill'), +] + +websocket_urlpatterns = [ + path('orders//', OrderConsumer.as_asgi()), +] diff --git a/restaurant_orders/dashboard/views.py b/restaurant_orders/dashboard/views.py new file mode 100644 index 0000000..cfbc757 --- /dev/null +++ b/restaurant_orders/dashboard/views.py @@ -0,0 +1,95 @@ +from django.shortcuts import render, redirect, reverse +from django.contrib.auth.mixins import LoginRequiredMixin +from django.views.generic.list import ListView, View +from django.views.generic.edit import UpdateView +from django.contrib.messages.views import SuccessMessageMixin +from django.core import serializers +from django.contrib import messages + + +from dashboard.forms import OrderStatusForm, AddToBillForm +from core.tasks import create_order_and_send_notification +from core.models import Restaurant, Order + +class Home(LoginRequiredMixin, View): + def get(self, request): + redirect_url = 'dashboard:restaurant_dashboard' + restaurants = Restaurant.get_user_restaurants(request.user) + + if len(restaurants) == 1: + return redirect(redirect_url, restaurant_pk=restaurants[0].pk) + + return render(request, template_name='restaurants_choice.html', context={ + 'title': 'Dashboard', + 'restaurants': restaurants, + 'redirect_url': redirect_url + }) + +class DashboardView(LoginRequiredMixin, ListView): + template_name = 'dashboard/dashboard.html' + model = Order + paginate_by = 4 + + def get_queryset(self, *args, **kwargs): + restaurant = Restaurant.get_user_restaurant_or_404(pk=self.kwargs.get('restaurant_pk'), + user=self.request.user) + + status = self.request.GET.get('status') + queryset = {} + if status: + queryset['wp_status'] = status + + obj = super().get_queryset(*args, **kwargs).filter( + restaurant=restaurant, + can_display=True, + **queryset + ).order_by('-wp_id') + + return obj + + +class DashboardOrderView(LoginRequiredMixin, View): + def get(self, request, pk): + order = Order.get_order(pk, request.user) + orderStatusForm = OrderStatusForm(instance=order) + addToBillForm = AddToBillForm(pk, request.user) + + return render(request, 'dashboard/dashboard_order.html', context={ + 'order': order, + 'orderStatusForm': orderStatusForm, + 'addToBillForm': addToBillForm + }) + + +class ChangeOrderStatusView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): + form_class = OrderStatusForm + model = Order + success_message = 'Zapisano!' + slug_field='order_pk' + + def get_queryset(self, *args, **kwargs): + return super().get_queryset(*args, **kwargs).filter( + pk=self.kwargs['pk'], + can_display=True, + restaurant__users=self.request.user.pk + ) + + def get_success_url(self): + return reverse('dashboard:order_dashboard', args=(self.kwargs['pk'], )) + +class AddToBillView(LoginRequiredMixin, View): + def post(self, request, pk, *args, **kwargs): + addToBillForm = AddToBillForm(pk, request.user, request.POST) + + if addToBillForm.is_valid(): + order = Order.get_order(pk, request.user) + order = serializers.serialize('json', (order, )) + email = True if addToBillForm.data.get('send_mail') else False + phone = True if addToBillForm.data.get('send_sms') else False + items = [(wp_pk, price) for wp_pk, price in request.POST.items() if price.isdigit()] + + # TODO: Za duzo tych jebanych argumentow ! + create_order_and_send_notification.delay(order, items, email, phone, request.user.pk) + + + return redirect('dashboard:order_dashboard', pk) diff --git a/restaurant_orders/manage.py b/restaurant_orders/manage.py new file mode 100755 index 0000000..5bacafc --- /dev/null +++ b/restaurant_orders/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'restaurant_orders.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/restaurant_orders/restaurant_orders/__init__.py b/restaurant_orders/restaurant_orders/__init__.py new file mode 100644 index 0000000..a10bec5 --- /dev/null +++ b/restaurant_orders/restaurant_orders/__init__.py @@ -0,0 +1 @@ +from . import celery diff --git a/restaurant_orders/restaurant_orders/asgi.py b/restaurant_orders/restaurant_orders/asgi.py new file mode 100644 index 0000000..37eda18 --- /dev/null +++ b/restaurant_orders/restaurant_orders/asgi.py @@ -0,0 +1,34 @@ +""" +ASGI config for restaurant_orders project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ +""" + +import os + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'restaurant_orders.settings') +import django +django.setup() + +from django.core.asgi import get_asgi_application + +import restaurant_orders.urls + +from channels.auth import AuthMiddlewareStack +from channels.routing import ProtocolTypeRouter, URLRouter +from channels.security.websocket import AllowedHostsOriginValidator + + +application = ProtocolTypeRouter({ + "http": get_asgi_application(), + "websocket": AllowedHostsOriginValidator( + AuthMiddlewareStack( + URLRouter( + restaurant_orders.urls.websocket_urlpatterns + ) + ) + ), +}) diff --git a/restaurant_orders/restaurant_orders/celery.py b/restaurant_orders/restaurant_orders/celery.py new file mode 100644 index 0000000..c00bb19 --- /dev/null +++ b/restaurant_orders/restaurant_orders/celery.py @@ -0,0 +1,10 @@ +import os +from celery import Celery +from django.conf import settings + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'restaurant_orders.settings') +app = Celery('restaurant_orders') + +app.config_from_object('django.conf:settings', namespace='CELERY') +app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) + diff --git a/restaurant_orders/restaurant_orders/consumers.py b/restaurant_orders/restaurant_orders/consumers.py new file mode 100644 index 0000000..8928c24 --- /dev/null +++ b/restaurant_orders/restaurant_orders/consumers.py @@ -0,0 +1,48 @@ +import json + +from channels.generic.websocket import AsyncWebsocketConsumer +from channels.exceptions import StopConsumer +import channels.layers + +from asgiref.sync import sync_to_async, async_to_sync + + +class NotificationsConsumer(AsyncWebsocketConsumer): + OK = 'ok' + ERROR = 'error' + WARNING = 'warning' + + async def connect(self): + self.room_group_name = '' + user = self.scope["user"] + if not user.is_authenticated: + return False + + self.room_name = f'user_{user.pk}' + self.room_group_name = f'notifications_{self.room_name}' + + # Join room group + await self.channel_layer.group_add( + self.room_group_name, + self.channel_name + ) + + await self.accept() + + async def disconnect(self, close_code): + raise StopConsumer + + async def notify(self, event): + await self.send(event['data']) + + @staticmethod + def send_notifications(user_pk, status, message): + layer = channels.layers.get_channel_layer() + + async_to_sync(layer.group_send)(f'notifications_user_{user_pk}', { + 'type': 'notify', + 'data': json.dumps({ + 'status': status, + 'message': message + }) + }) diff --git a/restaurant_orders/restaurant_orders/settings.py b/restaurant_orders/restaurant_orders/settings.py new file mode 100644 index 0000000..9f5e3e1 --- /dev/null +++ b/restaurant_orders/restaurant_orders/settings.py @@ -0,0 +1,177 @@ +""" +Django settings for restaurant_orders project. + +Generated by 'django-admin startproject' using Django 4.0.6. + +For more information on this file, see +https://docs.djangoproject.com/en/4.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.0/ref/settings/ +""" + +from pathlib import Path +import os + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = os.environ.get('SECRET_KEY') + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = bool(int(os.environ.get('DEBUG', 0))) + +ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',') + +LOGIN_URL = 'login' +LOGIN_REDIRECT_URL = 'home' +LOGOUT_REDIRECT_URL = 'home' + +REDIS_HOST = os.environ.get('REDIS_HOST') +ASGI_APPLICATION = "restaurant_orders.asgi.application" +CHANNEL_LAYERS = { + 'default': { + 'BACKEND': 'channels_redis.core.RedisChannelLayer', + 'CONFIG': { + "hosts": [(REDIS_HOST, 6379)], + }, + }, +} + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'core', + 'dashboard', + 'wordpress_integration', + 'settings', + 'channels' +] + + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + + +if DEBUG: + INSTALLED_APPS.append("debug_toolbar") + MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware") + + INTERNAL_IPS = [ + "127.0.0.1", + ] + + +ROOT_URLCONF = 'restaurant_orders.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'restaurant_orders' / 'templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'restaurant_orders.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/4.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': os.environ.get('DB_NAME'), + 'USER': os.environ.get('DB_USER'), + 'PASSWORD': os.environ.get('DB_PASSWORD'), + 'HOST': os.environ.get('DB_HOST'), + 'PORT': 5432 + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_HOST = os.environ.get('EMAIL_HOST') +EMAIL_HOST_USER = os.environ.get('EMAIL_USER') +EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_PASSWORD') +EMAIL_PORT = 587 +EMAIL_USE_TLS = True + + +TWILIO_ACCOUNT_SID = os.environ.get('TWILIO_ACCOUNT_SID') +TWILIO_TOKEN = os.environ.get('TWILIO_TOKEN') + +CELERY_BROKER_URL = f"redis://{REDIS_HOST}:6379/0" +CELERY_RESULT_BACKEND = f"redis://{REDIS_HOST}:6379/1" +CELERY_ACCEPT_CONTENT = ['application/json'] +CELERY_TASK_SERIALIZER = 'json' +CELERY_RESULT_SERIALIZER = 'json' +CELERY_TIMEZONE = 'Europe/Warsaw' + +# Internationalization +# https://docs.djangoproject.com/en/4.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.0/howto/static-files/ + +STATIC_URL = 'static/' +STATIC_ROOT = os.environ.get('STATIC_DIR') + +# Default primary key field type +# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field + +# DEFAULT_AUTO_FIELD = 'django.db.mod55s.BigAutoField' +DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' diff --git a/restaurant_orders/restaurant_orders/templates/_pagination.html b/restaurant_orders/restaurant_orders/templates/_pagination.html new file mode 100644 index 0000000..5c7e11e --- /dev/null +++ b/restaurant_orders/restaurant_orders/templates/_pagination.html @@ -0,0 +1,34 @@ +{% if page_obj %} + + + +{% else %} +
+ + Strona 0 z 0. + +
+{% endif %} diff --git a/restaurant_orders/restaurant_orders/templates/base.html b/restaurant_orders/restaurant_orders/templates/base.html new file mode 100644 index 0000000..9bea1ca --- /dev/null +++ b/restaurant_orders/restaurant_orders/templates/base.html @@ -0,0 +1,90 @@ + + + + + + {% block title %}{% endblock %} + + + + + + + +
+ + +
+ {% if messages %} +
+ {% for m in messages %} + {% if m.tags == 'success' %} + +
+ + + + + diff --git a/restaurant_orders/restaurant_orders/templates/home.html b/restaurant_orders/restaurant_orders/templates/home.html new file mode 100644 index 0000000..bceedf3 --- /dev/null +++ b/restaurant_orders/restaurant_orders/templates/home.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block title %}Home{% endblock %} + +{% block content %} +
+

Reami

+

Restaurant orders app

+
+{% endblock %} diff --git a/restaurant_orders/restaurant_orders/templates/login.html b/restaurant_orders/restaurant_orders/templates/login.html new file mode 100644 index 0000000..a48845a --- /dev/null +++ b/restaurant_orders/restaurant_orders/templates/login.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} + +{% block title %}Login{% endblock %} + +{% block content %} +
+
+

Login

+ {% csrf_token %} +
+ {{form.username.label_tag}} + {{form.username}} +
+
+ {{form.password.label_tag}} + {{form.password}} +
+ + +
+
+{% endblock %} diff --git a/restaurant_orders/restaurant_orders/templates/restaurants_choice.html b/restaurant_orders/restaurant_orders/templates/restaurants_choice.html new file mode 100644 index 0000000..0df5dc1 --- /dev/null +++ b/restaurant_orders/restaurant_orders/templates/restaurants_choice.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block title %}Wybierz restauracje{% endblock %} + +{% block content %} + {% if restaurants %} +

{{title}}

+

Wybierz restauracje

+
+ {% for restaurant in restaurants %} + + {% endfor %} +
+ {% else %} +

Nie posiadasz restauracji!

+ {% endif %} +{% endblock %} diff --git a/restaurant_orders/restaurant_orders/urls.py b/restaurant_orders/restaurant_orders/urls.py new file mode 100644 index 0000000..da50ea9 --- /dev/null +++ b/restaurant_orders/restaurant_orders/urls.py @@ -0,0 +1,45 @@ +"""restaurant_orders URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include +from django.contrib.auth.views import LoginView, LogoutView +from django.views.generic import TemplateView +from django.conf import settings +from django.conf.urls.static import static + +from restaurant_orders.consumers import NotificationsConsumer + +from channels.routing import URLRouter +import dashboard.urls + +urlpatterns = [ + path('admin/', admin.site.urls), + path('login/', LoginView.as_view(template_name='login.html'), name='login'), + path('logout/', LogoutView.as_view(), name='logout'), + path('dashboard/', include('dashboard.urls')), + path('settings/', include('settings.urls')), + path('webhook/', include('wordpress_integration.urls')), + path('', TemplateView.as_view(template_name='home.html'), name="home"), +] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + + +if settings.DEBUG: + urlpatterns.append(path('__debug__/', include('debug_toolbar.urls'))) + +websocket_urlpatterns = [ + path('ws/notifications/', NotificationsConsumer.as_asgi()), + path('ws/dashboard/', URLRouter(dashboard.urls.websocket_urlpatterns)), +] diff --git a/restaurant_orders/restaurant_orders/wsgi.py b/restaurant_orders/restaurant_orders/wsgi.py new file mode 100644 index 0000000..6f5b7b6 --- /dev/null +++ b/restaurant_orders/restaurant_orders/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for restaurant_orders project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'restaurant_orders.settings') + +application = get_wsgi_application() diff --git a/restaurant_orders/settings/__init__.py b/restaurant_orders/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/restaurant_orders/settings/admin.py b/restaurant_orders/settings/admin.py new file mode 100644 index 0000000..694323f --- /dev/null +++ b/restaurant_orders/settings/admin.py @@ -0,0 +1 @@ +from django.contrib import admin diff --git a/restaurant_orders/settings/apps.py b/restaurant_orders/settings/apps.py new file mode 100644 index 0000000..71a36cb --- /dev/null +++ b/restaurant_orders/settings/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class SettingsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'settings' diff --git a/restaurant_orders/settings/forms.py b/restaurant_orders/settings/forms.py new file mode 100644 index 0000000..1ea90e9 --- /dev/null +++ b/restaurant_orders/settings/forms.py @@ -0,0 +1,18 @@ +from django import forms +from core.models import Restaurant + +FORM_TAILWIND_CLASSES = 'form-control block w-full px-3 py-1.5 text-base font-normal text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 rounded transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none' + +class RestaurantForm(forms.ModelForm): + woocommerce_consumer_key = forms.CharField(required=False) + woocommerce_consumer_secret = forms.CharField(required=False) + woocommerce_webhook_secret = forms.CharField(required=False) + + class Meta: + model = Restaurant + exclude = ('users', ) + + def __init__(self, *args, **kwargs): + super(RestaurantForm, self).__init__(*args, **kwargs) + for key in self.fields.keys(): + self.fields[key].widget.attrs.update({'class': FORM_TAILWIND_CLASSES}) diff --git a/restaurant_orders/settings/templates/settings/restaurant_settings.html b/restaurant_orders/settings/templates/settings/restaurant_settings.html new file mode 100644 index 0000000..eebe63a --- /dev/null +++ b/restaurant_orders/settings/templates/settings/restaurant_settings.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} + +{% block title %}Settings{% endblock %} + +{% block content %} +
+

Settings

+ +
+ Wpisz ten wehbhook do ustawien w twoim sklepie! + {{request.scheme}}://{{request.get_host}}{% url 'wordpress_integration:webhook' restaurant_pk=view.kwargs.pk %} +
+ +
+
+ {% csrf_token %} + {{form.as_p}} + +
+
+
+{% endblock %} diff --git a/restaurant_orders/settings/urls.py b/restaurant_orders/settings/urls.py new file mode 100644 index 0000000..294ea14 --- /dev/null +++ b/restaurant_orders/settings/urls.py @@ -0,0 +1,9 @@ +from django.urls import path +from settings.views import Home, RestaurantSettings + +app_name = 'settings' + +urlpatterns = [ + path('', Home.as_view(), name='home'), + path('restaurant/', RestaurantSettings.as_view(), name='restaurant_settings') +] diff --git a/restaurant_orders/settings/views.py b/restaurant_orders/settings/views.py new file mode 100644 index 0000000..0921cd3 --- /dev/null +++ b/restaurant_orders/settings/views.py @@ -0,0 +1,44 @@ +from django.urls import reverse +from django.shortcuts import render, redirect, Http404 +from django.views.generic.edit import UpdateView, CreateView, View +from django.contrib.messages.views import SuccessMessageMixin +from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin + +from core.models import Restaurant +from settings.forms import RestaurantForm + + +class Home(LoginRequiredMixin, PermissionRequiredMixin, View): + permission_required = 'core.change_restaurant' + + def get(self, request): + if not self.request.user.has_perm('settings.change_restaurant'): + raise Http404 + + redirect_url = 'settings:restaurant_settings' + restaurants = Restaurant.get_user_restaurants(request.user) + + if len(restaurants) == 1: + return redirect(redirect_url, pk=restaurants[0].pk) + + return render(request, template_name='restaurants_choice.html', context={ + 'title': 'Ustawienia', + 'restaurants': restaurants, + 'redirect_url': redirect_url + }) + +class RestaurantSettings(LoginRequiredMixin, SuccessMessageMixin, PermissionRequiredMixin, UpdateView): + template_name = 'settings/restaurant_settings.html' + form_class = RestaurantForm + model = Restaurant + success_message = 'Zapisano!' + permission_required = 'core.change_restaurant' + + def get_queryset(self, *args, **kwargs): + return super().get_queryset(*args, **kwargs).filter( + pk=self.kwargs['pk'], + users=self.request.user.pk + ) + + def get_success_url(self): + return reverse('settings:restaurant_settings', args=(self.kwargs['pk'], )) diff --git a/restaurant_orders/wordpress_integration/__init__.py b/restaurant_orders/wordpress_integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/restaurant_orders/wordpress_integration/admin.py b/restaurant_orders/wordpress_integration/admin.py new file mode 100644 index 0000000..694323f --- /dev/null +++ b/restaurant_orders/wordpress_integration/admin.py @@ -0,0 +1 @@ +from django.contrib import admin diff --git a/restaurant_orders/wordpress_integration/apps.py b/restaurant_orders/wordpress_integration/apps.py new file mode 100644 index 0000000..f2fd48f --- /dev/null +++ b/restaurant_orders/wordpress_integration/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class WordpressIntegrationConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'wordpress_integration' diff --git a/restaurant_orders/wordpress_integration/urls.py b/restaurant_orders/wordpress_integration/urls.py new file mode 100644 index 0000000..2489b9a --- /dev/null +++ b/restaurant_orders/wordpress_integration/urls.py @@ -0,0 +1,8 @@ +from wordpress_integration.views import webhook +from django.urls import path + +app_name = 'wordpress_integration' + +urlpatterns = [ + path('/', webhook, name='webhook'), +] diff --git a/restaurant_orders/wordpress_integration/views.py b/restaurant_orders/wordpress_integration/views.py new file mode 100644 index 0000000..de6695f --- /dev/null +++ b/restaurant_orders/wordpress_integration/views.py @@ -0,0 +1,22 @@ +from core.models import Restaurant, Order +from core.decorators import woocommerce_authentication_required + +from django.shortcuts import HttpResponse, get_object_or_404 +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_POST +import json + +@csrf_exempt +@require_POST +@woocommerce_authentication_required +def webhook(request, restaurant_pk): + payload = request.body.decode('utf-8') + restaurant = get_object_or_404(Restaurant, pk=restaurant_pk) + + order = Order.update_or_create_from_response(json.loads(payload), restaurant) + if order is None: + response = HttpResponse('Error, cannot read order properties!') + response.status_code = 400 + return response + + return HttpResponse('success') -- cgit v1.2.3