diff options
49 files changed, 1671 insertions, 0 deletions
diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..048cd6f --- /dev/null +++ b/Dockerfile | |||
@@ -0,0 +1,26 @@ | |||
1 | FROM python:3 | ||
2 | LABEL maintainer="jdlugosz963@gmail.com" | ||
3 | |||
4 | WORKDIR /usr/src/app | ||
5 | |||
6 | COPY ./scripts /usr/src/scripts | ||
7 | RUN chmod +x /usr/src/scripts/* | ||
8 | ENV PATH /usr/src/scripts:$PATH | ||
9 | |||
10 | COPY requirements.txt . | ||
11 | RUN pip install -r requirements.txt | ||
12 | |||
13 | COPY ./restaurant_orders/ . | ||
14 | |||
15 | |||
16 | RUN useradd nonrootuser | ||
17 | |||
18 | RUN mkdir -p /vol/web/static | ||
19 | RUN chown -R nonrootuser:nonrootuser /vol | ||
20 | RUN chmod -R 755 /vol | ||
21 | |||
22 | RUN chown -R nonrootuser:nonrootuser /usr/src | ||
23 | |||
24 | USER nonrootuser | ||
25 | |||
26 | CMD ["entrypoint.sh"] | ||
diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml new file mode 100644 index 0000000..26c817d --- /dev/null +++ b/docker-compose-dev.yml | |||
@@ -0,0 +1,69 @@ | |||
1 | version: '3.8' | ||
2 | |||
3 | services: | ||
4 | app: | ||
5 | build: . | ||
6 | volumes: | ||
7 | - static_data:/vol/web | ||
8 | - ./restaurant_orders/:/usr/src/app | ||
9 | command: "python manage.py runserver 0.0.0.0:8000" | ||
10 | environment: | ||
11 | - DEBUG=1 | ||
12 | - STATIC_DIR=/vol/web | ||
13 | - REDIS_HOST=redis | ||
14 | - DB_POSTGRES=1 | ||
15 | - DB_NAME=django | ||
16 | - DB_USER=admin | ||
17 | - DB_PASSWORD=admin | ||
18 | - DB_HOST=db | ||
19 | depends_on: | ||
20 | - redis | ||
21 | - db | ||
22 | ports: | ||
23 | - "8000:8000" | ||
24 | links: | ||
25 | - redis | ||
26 | - db | ||
27 | db: | ||
28 | image: postgres:latest | ||
29 | volumes: | ||
30 | - pg_data:/var/lib/postgresql/data/ | ||
31 | environment: | ||
32 | - POSTGRES_DB=django | ||
33 | - POSTGRES_USER=admin | ||
34 | - POSTGRES_PASSWORD=admin | ||
35 | |||
36 | redis: | ||
37 | image: redis:latest | ||
38 | |||
39 | celery_worker: | ||
40 | build: . | ||
41 | command: "celery.sh" | ||
42 | volumes: | ||
43 | - ./restaurant_orders/:/usr/src/app | ||
44 | environment: | ||
45 | - DEBUG=1 | ||
46 | - REDIS_HOST=redis | ||
47 | depends_on: | ||
48 | - redis | ||
49 | - db | ||
50 | |||
51 | # pgadmin: | ||
52 | # container_name: pgadmin4_container | ||
53 | # image: dpage/pgadmin4 | ||
54 | # restart: always | ||
55 | # environment: | ||
56 | # PGADMIN_DEFAULT_EMAIL: jdlugosz963@gmail.com | ||
57 | # PGADMIN_DEFAULT_PASSWORD: 1234 | ||
58 | # PGADMIN_LISTEN_PORT: 80 | ||
59 | # ports: | ||
60 | # - "8080:80" | ||
61 | # volumes: | ||
62 | # - pgadmin_data:/var/lib/pgadmin | ||
63 | # links: | ||
64 | # - db | ||
65 | |||
66 | volumes: | ||
67 | static_data: | ||
68 | pg_data: | ||
69 | # pgadmin_data: | ||
diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8b6bc6e --- /dev/null +++ b/docker-compose.yml | |||
@@ -0,0 +1,67 @@ | |||
1 | version: '3.9' | ||
2 | |||
3 | services: | ||
4 | app: | ||
5 | build: | ||
6 | context: . | ||
7 | restart: always | ||
8 | volumes: | ||
9 | - static_data:/vol/web | ||
10 | environment: | ||
11 | - TWILIO_TOKEN=${TWILIO_TOKEN} | ||
12 | - TWILIO_ACCOUNT_SID=${TWILIO_ACCOUNT_SID} | ||
13 | |||
14 | - EMAIL_HOST=${EMAIL_HOST} | ||
15 | - EMIAL_USER=${EMIAL_USER} | ||
16 | - EMAIL_PASSWORD=${EMAIL_PASSWORD} | ||
17 | |||
18 | - REDIS_HOST=${REDIS_HOST} | ||
19 | - SECRET_KEY=${SECRET_KEY} | ||
20 | - ALLOWED_HOSTS=${ALLOWED_HOSTS} | ||
21 | - STATIC_DIR=/vol/web/static | ||
22 | |||
23 | - DB_NAME=${DB_NAME} | ||
24 | - DB_USER=${DB_USER} | ||
25 | - DB_PASSWORD=${DB_PASSWORD} | ||
26 | - DB_HOST=${DB_HOST} | ||
27 | depends_on: | ||
28 | - redis | ||
29 | - db | ||
30 | |||
31 | celery_worker: | ||
32 | build: . | ||
33 | restart: always | ||
34 | command: "celery.sh" | ||
35 | environment: | ||
36 | - REDIS_HOST=${REDIS_HOST} | ||
37 | depends_on: | ||
38 | - redis | ||
39 | - db | ||
40 | |||
41 | db: | ||
42 | image: postgres:latest | ||
43 | restart: always | ||
44 | volumes: | ||
45 | - pg_data:/var/lib/postgresql/data/ | ||
46 | environment: | ||
47 | - POSTGRES_DB=${DB_NAME} | ||
48 | - POSTGRES_USER=${DB_USER} | ||
49 | - POSTGRES_PASSWORD=${DB_PASSWORD} | ||
50 | |||
51 | redis: | ||
52 | image: redis:6-alpine | ||
53 | |||
54 | proxy: | ||
55 | build: | ||
56 | context: ./proxy | ||
57 | restart: always | ||
58 | volumes: | ||
59 | - static_data:/vol/web | ||
60 | ports: | ||
61 | - "80:80" | ||
62 | depends_on: | ||
63 | - app | ||
64 | |||
65 | volumes: | ||
66 | static_data: | ||
67 | pg_data: | ||
diff --git a/proxy/Dockerfile b/proxy/Dockerfile new file mode 100644 index 0000000..ce56023 --- /dev/null +++ b/proxy/Dockerfile | |||
@@ -0,0 +1,12 @@ | |||
1 | FROM nginxinc/nginx-unprivileged:latest | ||
2 | LABEL maintainer="jdlugosz963@gmail.com" | ||
3 | |||
4 | COPY ./default.conf /etc/nginx/conf.d/default.conf | ||
5 | |||
6 | USER root | ||
7 | |||
8 | RUN mkdir -p /vol/static | ||
9 | RUN chmod 755 /vol/static | ||
10 | |||
11 | USER nginx | ||
12 | |||
diff --git a/proxy/default.conf b/proxy/default.conf new file mode 100644 index 0000000..2e30df3 --- /dev/null +++ b/proxy/default.conf | |||
@@ -0,0 +1,29 @@ | |||
1 | upstream app { | ||
2 | server app:8000; | ||
3 | } | ||
4 | |||
5 | server { | ||
6 | listen 80; | ||
7 | |||
8 | location /static { | ||
9 | alias /vol/web/static; | ||
10 | } | ||
11 | |||
12 | location / { | ||
13 | try_files $uri @proxy_to_app; | ||
14 | } | ||
15 | |||
16 | location @proxy_to_app { | ||
17 | proxy_pass http://app; | ||
18 | |||
19 | proxy_http_version 1.1; | ||
20 | proxy_set_header Upgrade $http_upgrade; | ||
21 | proxy_set_header Connection "upgrade"; | ||
22 | |||
23 | proxy_redirect off; | ||
24 | proxy_set_header Host $host; | ||
25 | proxy_set_header X-Real-IP $remote_addr; | ||
26 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | ||
27 | proxy_set_header X-Forwarded-Host $server_name; | ||
28 | } | ||
29 | } \ No newline at end of file | ||
diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..fd8278b --- /dev/null +++ b/requirements.txt | |||
@@ -0,0 +1,55 @@ | |||
1 | aioredis==1.3.1 | ||
2 | amqp==5.1.1 | ||
3 | asgiref==3.5.2 | ||
4 | async-timeout==4.0.2 | ||
5 | attrs==22.1.0 | ||
6 | autobahn==22.7.1 | ||
7 | Automat==20.2.0 | ||
8 | billiard==3.6.4.0 | ||
9 | celery==5.2.7 | ||
10 | certifi==2022.6.15 | ||
11 | cffi==1.15.1 | ||
12 | channels==3.0.5 | ||
13 | channels-redis==3.4.1 | ||
14 | charset-normalizer==2.1.1 | ||
15 | click==8.1.3 | ||
16 | click-didyoumean==0.3.0 | ||
17 | click-plugins==1.1.1 | ||
18 | click-repl==0.2.0 | ||
19 | constantly==15.1.0 | ||
20 | cryptography==37.0.4 | ||
21 | daphne==3.0.2 | ||
22 | Deprecated==1.2.13 | ||
23 | Django==4.1 | ||
24 | django-debug-toolbar==3.6.0 | ||
25 | hiredis==2.0.0 | ||
26 | hyperlink==21.0.0 | ||
27 | idna==3.3 | ||
28 | incremental==21.3.0 | ||
29 | kombu==5.2.4 | ||
30 | msgpack==1.0.4 | ||
31 | packaging==21.3 | ||
32 | prompt-toolkit==3.0.30 | ||
33 | psycopg2==2.9.3 | ||
34 | pyasn1==0.4.8 | ||
35 | pyasn1-modules==0.2.8 | ||
36 | pycparser==2.21 | ||
37 | PyJWT==2.4.0 | ||
38 | pyOpenSSL==22.0.0 | ||
39 | pyparsing==3.0.9 | ||
40 | pytz==2022.2.1 | ||
41 | redis==4.3.4 | ||
42 | requests==2.28.1 | ||
43 | service-identity==21.1.0 | ||
44 | six==1.16.0 | ||
45 | sqlparse==0.4.2 | ||
46 | twilio==7.12.1 | ||
47 | Twisted==22.4.0 | ||
48 | txaio==22.2.1 | ||
49 | typing_extensions==4.3.0 | ||
50 | urllib3==1.26.12 | ||
51 | vine==5.0.0 | ||
52 | wcwidth==0.2.5 | ||
53 | WooCommerce==3.0.0 | ||
54 | wrapt==1.14.1 | ||
55 | zope.interface==5.4.0 | ||
diff --git a/restaurant_orders/core/__init__.py b/restaurant_orders/core/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/restaurant_orders/core/__init__.py | |||
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 @@ | |||
1 | from django.contrib import admin | ||
2 | from core.models import Restaurant, Order | ||
3 | |||
4 | admin.site.register(Restaurant) | ||
5 | 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 @@ | |||
1 | from django.apps import AppConfig | ||
2 | |||
3 | |||
4 | class CoreConfig(AppConfig): | ||
5 | default_auto_field = 'django.db.models.BigAutoField' | ||
6 | 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 @@ | |||
1 | from django.http import HttpResponse | ||
2 | from core.models import Restaurant | ||
3 | |||
4 | import base64 | ||
5 | import hashlib | ||
6 | import hmac | ||
7 | |||
8 | def get_webhook_secret_from_restaurant(pk): | ||
9 | try: | ||
10 | token = Restaurant.objects.get(pk=pk).woocommerce_webhook_secret | ||
11 | if token != '': | ||
12 | return token | ||
13 | except Restaurant.DoesNotExist: | ||
14 | pass | ||
15 | return None | ||
16 | |||
17 | def compare_signatures(body, webhook_secret, request_sig): | ||
18 | signature = hmac.new(webhook_secret.encode(), body, hashlib.sha256).digest() | ||
19 | return hmac.compare_digest(request_sig.encode(), base64.b64encode(signature)) | ||
20 | |||
21 | |||
22 | def woocommerce_authentication_required(view): | ||
23 | def inner(request, restaurant_pk, *args, **kwargs): | ||
24 | webhook_secret = get_webhook_secret_from_restaurant(restaurant_pk) | ||
25 | request_sig = request.headers.get('x-wc-webhook-signature') | ||
26 | |||
27 | if not webhook_secret or not request_sig: | ||
28 | return HttpResponse('Unauthorized') | ||
29 | |||
30 | if compare_signatures(request.body, webhook_secret, request_sig): | ||
31 | return view(request, restaurant_pk, *args, **kwargs) | ||
32 | |||
33 | response = HttpResponse('Unauthorized') | ||
34 | response.status_code = 403 | ||
35 | return response | ||
36 | 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 @@ | |||
1 | from django.db import models | ||
2 | from django.contrib.auth.models import User | ||
3 | from django.shortcuts import Http404, get_object_or_404 | ||
4 | |||
5 | from datetime import datetime | ||
6 | |||
7 | |||
8 | class Restaurant(models.Model): | ||
9 | name = models.CharField(max_length=50) | ||
10 | users = models.ManyToManyField(User) | ||
11 | |||
12 | wordpress_url = models.URLField(max_length=50, unique=True) | ||
13 | woocommerce_consumer_key = models.CharField(max_length=50) | ||
14 | woocommerce_consumer_secret = models.CharField(max_length=50) | ||
15 | woocommerce_webhook_secret = models.CharField(max_length=50) | ||
16 | |||
17 | @classmethod | ||
18 | def get_user_restaurants(cls, user: User): | ||
19 | return cls.objects.filter(users=user) | ||
20 | |||
21 | @classmethod | ||
22 | def get_user_restaurant_or_404(cls, pk, user: User): | ||
23 | return get_object_or_404(cls, pk=pk, users=user) | ||
24 | |||
25 | def __str__(self): | ||
26 | return self.name | ||
27 | |||
28 | |||
29 | class Order(models.Model): | ||
30 | WP_STATES = ( | ||
31 | ('pending', 'Oczekujace'), | ||
32 | ('processing', 'Przetwarzane'), | ||
33 | ('on-hold', 'Wstrzymane'), | ||
34 | ('completed', 'Zakonczone'), | ||
35 | ('cancelled', 'Anulowane'), | ||
36 | ('refunded', 'Zwrocone'), | ||
37 | ('failed', 'Nie powiodlo sie'), | ||
38 | ('trash', 'Usuniete') | ||
39 | ) | ||
40 | |||
41 | wp_id = models.IntegerField(editable=False) | ||
42 | wp_status = models.CharField(max_length=30, choices=WP_STATES) | ||
43 | wp_order_key = models.CharField(max_length=50, editable=False) | ||
44 | date_created = models.DateField(editable=False) | ||
45 | date_modified = models.DateField() | ||
46 | line_items = models.JSONField() | ||
47 | billing = models.JSONField() | ||
48 | restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE) | ||
49 | can_display = models.BooleanField(default=True) | ||
50 | |||
51 | @classmethod | ||
52 | def update_or_create_from_response(cls, response, restaurant_model): | ||
53 | try: | ||
54 | obj = cls.objects.update_or_create( | ||
55 | wp_id=response['id'], | ||
56 | wp_order_key=response['order_key'], | ||
57 | restaurant=restaurant_model, | ||
58 | defaults={ | ||
59 | 'wp_status': response['status'], | ||
60 | 'date_created': datetime.strptime(response['date_created'], '%Y-%m-%dT%H:%M:%S'), | ||
61 | 'date_modified': datetime.strptime(response['date_modified'], '%Y-%m-%dT%H:%M:%S'), | ||
62 | 'line_items': response['line_items'], | ||
63 | 'billing': response['billing'], | ||
64 | } | ||
65 | ) | ||
66 | return obj | ||
67 | except KeyError: | ||
68 | return None | ||
69 | |||
70 | |||
71 | @classmethod | ||
72 | def create_from_response_disable_view(cls, response, restaurant_model): | ||
73 | obj, _ = cls.update_or_create_from_response(response, restaurant_model) | ||
74 | obj.can_display = False | ||
75 | obj.save() | ||
76 | return obj | ||
77 | |||
78 | @classmethod | ||
79 | def get_order(cls, pk, user: User): | ||
80 | try: | ||
81 | return cls.objects.get( | ||
82 | pk=pk, | ||
83 | can_display=True, | ||
84 | restaurant__users=user | ||
85 | ) | ||
86 | except cls.DoesNotExist: | ||
87 | raise Http404 | ||
88 | |||
89 | def __str__(self): | ||
90 | 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 @@ | |||
1 | from celery import shared_task | ||
2 | |||
3 | from django.core import serializers | ||
4 | |||
5 | from restaurant_orders.consumers import NotificationsConsumer | ||
6 | |||
7 | from core.utils import Orders, SendMail, SendSms | ||
8 | import re | ||
9 | |||
10 | |||
11 | def send_notification(is_success, message, user_pk): | ||
12 | status = NotificationsConsumer.OK if is_success else NotificationsConsumer.ERROR | ||
13 | NotificationsConsumer.send_notifications( | ||
14 | user_pk, | ||
15 | status, | ||
16 | message | ||
17 | ) | ||
18 | |||
19 | |||
20 | |||
21 | @shared_task | ||
22 | def create_order_and_send_notification(order, items, is_email=None, is_sms=None, user_pk=None): | ||
23 | order = [obj for obj in serializers.deserialize('json', order)] | ||
24 | if len(order) != 1: | ||
25 | return | ||
26 | order = order[0].object | ||
27 | |||
28 | phone = order.billing.get('phone') | ||
29 | email = order.billing.get('email') | ||
30 | new_order = Orders(order.restaurant, order.billing).create_custom_order(items) | ||
31 | |||
32 | |||
33 | # if new_order is None: | ||
34 | # send_notification(False, | ||
35 | # "Niestety nie udalo sie skontaktowac z restauracja, prosze sprowbowac ponownie pozniej.", | ||
36 | # user_pk) | ||
37 | # return | ||
38 | |||
39 | EMAIL_REGEX = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b' | ||
40 | |||
41 | if is_sms: # TODO: Make regex for sms | ||
42 | sms = SendSms(new_order).send() | ||
43 | send_notification(*sms, user_pk) | ||
44 | |||
45 | if is_email and re.fullmatch(EMAIL_REGEX, str(email)): | ||
46 | mail = SendMail(new_order).send() | ||
47 | send_notification(*mail, user_pk) | ||
48 | |||
49 | |||
50 | |||
51 | |||
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 @@ | |||
1 | import json | ||
2 | from woocommerce import API | ||
3 | from twilio.rest import Client | ||
4 | from twilio.base.exceptions import TwilioRestException | ||
5 | |||
6 | from django.core.mail import send_mail | ||
7 | from django.conf import settings | ||
8 | |||
9 | from core.models import Order | ||
10 | |||
11 | class Sender(): | ||
12 | def __init__(self, order): | ||
13 | self.order = order | ||
14 | |||
15 | def get_order_url(self): | ||
16 | order_id = self.order.wp_id | ||
17 | order_key = self.order.wp_order_key | ||
18 | restaurant_url = self.order.restaurant.wordpress_url | ||
19 | return f'{restaurant_url}/zamowienie/order-pay/{order_id}/?pay_for_order=true&key={order_key}' | ||
20 | |||
21 | def get_message_body(self): | ||
22 | return f'Prosze dokonac platnosci: {self.get_order_url()}' | ||
23 | |||
24 | def send(self) -> (bool, str): | ||
25 | pass | ||
26 | |||
27 | |||
28 | class SendSms(Sender): | ||
29 | def __init__(self, order): | ||
30 | account_sid = settings.TWILIO_ACCOUNT_SID | ||
31 | auth_token = settings.TWILIO_TOKEN | ||
32 | |||
33 | self.client = Client(account_sid, auth_token) | ||
34 | self.from_ = "+17432007359" | ||
35 | |||
36 | super().__init__(order) | ||
37 | |||
38 | def send(self) -> (bool, str): | ||
39 | phone = self.order.billing.get('phone', None) | ||
40 | phone = "+48609155122" | ||
41 | if phone: | ||
42 | try: | ||
43 | message = self.client.messages.create(to=phone, | ||
44 | from_=self.from_, | ||
45 | body=self.get_message_body()) | ||
46 | except TwilioRestException as err: | ||
47 | return (False, err.msg) | ||
48 | else: | ||
49 | return (True, 'Wyslano sms') | ||
50 | return (False, 'Nie znaleziono numeru telefonu.') | ||
51 | |||
52 | |||
53 | |||
54 | class SendMail(Sender): | ||
55 | def send(self) -> (bool, str): | ||
56 | email = self.order.billing.get('email', None) | ||
57 | email = 'jdlugosz963@gmail.com' | ||
58 | if email: # Jesli sie spierdoli to wypluje | ||
59 | try: | ||
60 | send_mail('Strona do zaplaty', self.get_message_body(), 'no-reply@reami.pl', (email, ), fail_silently=False) | ||
61 | except smtplib.SMTPException: | ||
62 | return (False, "Niestety nie udalo sie wyslac maila.") | ||
63 | else: | ||
64 | return (True, "Wyslano maila.") | ||
65 | return (False, "Nie znaleziono maila.") | ||
66 | |||
67 | |||
68 | class Orders: | ||
69 | def __init__(self, restaurant, billing): | ||
70 | self.restaurant = restaurant | ||
71 | self.billing = billing | ||
72 | |||
73 | self.wcapi = API( | ||
74 | url=restaurant.wordpress_url, | ||
75 | consumer_key=restaurant.woocommerce_consumer_key, | ||
76 | consumer_secret=restaurant.woocommerce_consumer_secret, | ||
77 | timeout=7 | ||
78 | ) | ||
79 | |||
80 | def get_custom_order_data(self, items): | ||
81 | return { | ||
82 | "payment_method": "bacs", | ||
83 | "payment_method_title": "Direct Bank Transfer", | ||
84 | "set_paid": False, | ||
85 | "billing": self.billing, | ||
86 | "shipping": self.billing, | ||
87 | "line_items": [ | ||
88 | { | ||
89 | "product_id": pk, | ||
90 | "total": total, | ||
91 | "quantity": 1, | ||
92 | } for pk, total in items | ||
93 | ] | ||
94 | } | ||
95 | |||
96 | def create_custom_order(self, items): | ||
97 | data = self.get_custom_order_data(items) | ||
98 | response = self.wcapi.post("orders", data=data).json() | ||
99 | return Order.create_from_response_disable_view(response, self.restaurant) | ||
100 | |||
diff --git a/restaurant_orders/dashboard/__init__.py b/restaurant_orders/dashboard/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/restaurant_orders/dashboard/__init__.py | |||
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 @@ | |||
1 | from django.contrib import admin | ||
2 | |||
3 | # 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 @@ | |||
1 | from django.apps import AppConfig | ||
2 | |||
3 | |||
4 | class DashboardConfig(AppConfig): | ||
5 | default_auto_field = 'django.db.models.BigAutoField' | ||
6 | 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 @@ | |||
1 | from channels.generic.websocket import AsyncWebsocketConsumer | ||
2 | from channels.exceptions import StopConsumer | ||
3 | import channels.layers | ||
4 | |||
5 | from asgiref.sync import sync_to_async, async_to_sync | ||
6 | |||
7 | from core.models import Restaurant, Order | ||
8 | |||
9 | from django.db.models import signals | ||
10 | from django.dispatch import receiver | ||
11 | from django.core import serializers | ||
12 | |||
13 | |||
14 | class OrderConsumer(AsyncWebsocketConsumer): | ||
15 | async def connect(self): | ||
16 | restaurant_pk = self.scope['url_route']['kwargs']['restaurant_pk'] | ||
17 | self.restaurant = sync_to_async(Restaurant.get_user_restaurant_or_404)(restaurant_pk, self.scope['user']) | ||
18 | if not self.restaurant: | ||
19 | return | ||
20 | |||
21 | self.room_name = str(restaurant_pk) | ||
22 | self.room_group_name = f'restaurant_{self.room_name}' | ||
23 | |||
24 | # Join room group | ||
25 | await self.channel_layer.group_add( | ||
26 | self.room_group_name, | ||
27 | self.channel_name | ||
28 | ) | ||
29 | |||
30 | await self.accept() | ||
31 | |||
32 | async def disconnect(self, close_code): | ||
33 | raise StopConsumer | ||
34 | |||
35 | async def new_order(self, event): | ||
36 | await self.send(event['data']) | ||
37 | |||
38 | @staticmethod | ||
39 | @receiver(signals.post_save, sender=Order) | ||
40 | def order_observer(sender, instance, **kwargs): | ||
41 | if not instance.can_display: | ||
42 | return | ||
43 | |||
44 | layer = channels.layers.get_channel_layer() | ||
45 | |||
46 | async_to_sync(layer.group_send)(f'restaurant_{instance.restaurant.pk}', { | ||
47 | 'type': 'new.order', | ||
48 | 'data': serializers.serialize('json', (instance, )) | ||
49 | }) | ||
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 @@ | |||
1 | from django import forms | ||
2 | |||
3 | from core.models import Order | ||
4 | |||
5 | |||
6 | 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' | ||
7 | |||
8 | class OrderStatusForm(forms.ModelForm): | ||
9 | class Meta: | ||
10 | model = Order | ||
11 | fields = ('wp_status', ) | ||
12 | |||
13 | def __init__(self, *args, **kwargs): | ||
14 | super().__init__(*args, **kwargs) | ||
15 | self.fields['wp_status'].label = 'Przenies do:' | ||
16 | |||
17 | class AddToBillForm(forms.Form): | ||
18 | send_mail = forms.BooleanField(label='Wyslij maila', initial=False, required=False) | ||
19 | send_sms = forms.BooleanField(label='Wyslij sms', initial=True, required=False) | ||
20 | |||
21 | def __init__(self, pk, user, *args, **kwargs): | ||
22 | super().__init__(*args, **kwargs) | ||
23 | order = Order.get_order(pk, user) | ||
24 | |||
25 | for item in order.line_items: | ||
26 | index = item['product_id'] | ||
27 | self.fields[index] = forms.IntegerField(required=False, label=item['name']) | ||
28 | |||
29 | for index in self.fields.keys(): | ||
30 | 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 @@ | |||
1 | {% extends "base.html" %} | ||
2 | |||
3 | {% block title %}Dashboard{% endblock %} | ||
4 | |||
5 | {% block content %} | ||
6 | {% include '_pagination.html' %} | ||
7 | <form method="get" id="" action="" class="font-bold"> | ||
8 | <select name="status" class="bg-gray-50 rounded-lg p-2 pl-4 pr-4" onchange="this.form.submit()" id="id_status"> | ||
9 | |||
10 | <option value="" selected>Wszystkie</option> | ||
11 | <option value="pending">Oczekujace</option> | ||
12 | <option value="processing">Przetwarzane</option> | ||
13 | <option value="on-hold">Wstrzymane</option> | ||
14 | <option value="completed">Zakonczone</option> | ||
15 | <option value="cancelled">Anulowane</option> | ||
16 | <option value="refunded">Zwrocone</option> | ||
17 | <option value="failed">Nie powiodlo sie</option> | ||
18 | <option value="trash">Usuniete</option> | ||
19 | </select> | ||
20 | </form> | ||
21 | |||
22 | <script type="text/javascript"> | ||
23 | const $select = document.querySelector('#id_status'); | ||
24 | const paramsString = window.location.search; | ||
25 | const searchParams = new URLSearchParams(paramsString); | ||
26 | let status = searchParams.get('status'); | ||
27 | if(status != null) | ||
28 | $select.value = searchParams.get('status'); | ||
29 | </script> | ||
30 | |||
31 | <div id="orders" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 max-h-0 text-center h-full bg-gray-50 text-gray-600"> | ||
32 | {% for object in object_list %} | ||
33 | <div id="order_{{object.pk}}" class='order'> | ||
34 | <a href="{% url 'dashboard:order_dashboard' object.pk %}"> | ||
35 | <div class="flex m-10"> | ||
36 | <div class="bg-local block rounded-lg shadow-lg p-10 max-w-sm bg-gray-50 hover:bg-gray-100 hover:shadow-lg focus:bg-gray-200 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-gray-200 active:shadow-lg transition duration-150 ease-in-out"> | ||
37 | <h1 class="text-2xl leading-tight w-max font-bold">#{{ object.wp_id}} - {{ object.billing.first_name }} {{ object.billing.last_name }}</h1> | ||
38 | <h2 class="text-lg font-bold w-max mb-4">{{object.get_wp_status_display}}</h2> | ||
39 | <ul class="list-decimal"> | ||
40 | {% for item in object.line_items %} | ||
41 | <li class="text-sm leading-tight font-bold pt-1 float-left">{{ item.name }} x{{item.quantity}}szt.</li> | ||
42 | {% endfor %} | ||
43 | </ul> | ||
44 | </div> | ||
45 | </div> | ||
46 | </a> | ||
47 | </div> | ||
48 | {% endfor %} | ||
49 | </div> | ||
50 | <h3 id="empty_orders_list_message" class="hidden text-3xl font-bold py-20 mb-8">Na poczatku byla ciemnosc...</h3> | ||
51 | |||
52 | <script type="text/javascript"> | ||
53 | function setup_message_if_orders_empty() { | ||
54 | let orders = document.getElementById(`orders`) | ||
55 | let message = document.getElementById(`empty_orders_list_message`) | ||
56 | |||
57 | if (orders.getElementsByClassName('order').length == 0) | ||
58 | message.classList.remove('hidden') | ||
59 | else | ||
60 | message.classList.add('hidden') | ||
61 | } | ||
62 | |||
63 | function get_status_display(status) { | ||
64 | console.log(status) | ||
65 | let status_map = { | ||
66 | 'pending': 'Oczekujace', | ||
67 | 'processing': 'Przetwarzane', | ||
68 | 'on-hold': 'Wstrzymane', | ||
69 | 'completed': 'Zakonczone', | ||
70 | 'cancelled': 'Anulowane', | ||
71 | 'refunded': 'Zwrocone', | ||
72 | 'failed': 'Nie powiodlo sie', | ||
73 | 'trash': 'Usuniete' | ||
74 | } | ||
75 | return status_map[status] | ||
76 | } | ||
77 | function get_url(id) { | ||
78 | return `/dashboard/restaurant/order/${id}/` | ||
79 | } | ||
80 | |||
81 | function get_line_items_html(line_items) { | ||
82 | let data = '' | ||
83 | for (i in line_items) { | ||
84 | let item = line_items[i]; | ||
85 | data += `<li class="text-sm leading-tight font-bold pt-1 float-left">${item.name} x${item.quantity}szt.</li>` | ||
86 | } | ||
87 | return data | ||
88 | } | ||
89 | |||
90 | function add_order(data) { | ||
91 | let order = ` | ||
92 | <div id="order_${data.pk}" class='order'> | ||
93 | <a href="${get_url(data.pk)}"> | ||
94 | <div class="flex m-10"> | ||
95 | <div class="bg-local block rounded-lg shadow-lg p-10 max-w-sm bg-gray-50 hover:bg-gray-100 hover:shadow-lg focus:bg-gray-200 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-gray-200 active:shadow-lg transition duration-150 ease-in-out"> | ||
96 | <h1 class="text-2xl leading-tight w-max font-bold">#${data.fields.wp_id} - ${data.fields.billing.first_name} ${data.fields.billing.last_name}</h1> | ||
97 | <h2 class="text-lg font-bold w-max mb-4">${get_status_display(data.fields.wp_status)}</h2> | ||
98 | <ul class="list-decimal"> | ||
99 | ${get_line_items_html(data.fields.line_items)} | ||
100 | </ul> | ||
101 | </div> | ||
102 | </div> | ||
103 | </a> | ||
104 | </div> | ||
105 | ` | ||
106 | |||
107 | |||
108 | let order_with_pk_from_data = document.getElementById(`order_${data.pk}`) | ||
109 | |||
110 | const paramsString = window.location.search; | ||
111 | const searchParams = new URLSearchParams(paramsString); | ||
112 | status = searchParams.get('status') | ||
113 | if(status != data.fields.status && status) { | ||
114 | let order_container = document.getElementById(`orders`) | ||
115 | order_container.removeChild(order_with_pk_from_data) | ||
116 | setup_message_if_orders_empty() | ||
117 | return // TODO: add notification in future | ||
118 | } | ||
119 | if (order_with_pk_from_data) | ||
120 | order_with_pk_from_data.innerHTML = order | ||
121 | else { | ||
122 | let orders_container = document.getElementById('orders'); | ||
123 | orders_container.innerHTML += order; | ||
124 | } | ||
125 | setup_message_if_orders_empty() | ||
126 | return order | ||
127 | } | ||
128 | const socket = new WebSocket( | ||
129 | 'ws://' | ||
130 | + window.location.host | ||
131 | + '/ws/dashboard/orders/{{view.kwargs.restaurant_pk}}/' | ||
132 | ); | ||
133 | |||
134 | socket.onopen = function(e) { | ||
135 | console.log("[open] Connection established"); | ||
136 | }; | ||
137 | |||
138 | socket.onmessage = function(event) { | ||
139 | console.log('onmessage') | ||
140 | let data=JSON.parse(event.data); | ||
141 | console.log(data) | ||
142 | console.log(add_order(data[0])); | ||
143 | }; | ||
144 | socket.onclose = function(e) { | ||
145 | console.error('Chat socket closed unexpectedly'); | ||
146 | const data = JSON.parse(e.data); | ||
147 | console.log(data) | ||
148 | }; | ||
149 | |||
150 | setup_message_if_orders_empty(); | ||
151 | </script> | ||
152 | {% 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 @@ | |||
1 | {% extends "base.html" %} | ||
2 | |||
3 | {% block title %}Order #{{order.wp_id}}{% endblock %} | ||
4 | |||
5 | {% block content %} | ||
6 | <div> | ||
7 | <a href="javascript:window.history.back();">Wroc</a> | ||
8 | <h1 class="text-2xl leading-tight mb-4 pt-20 font-bold"><a href="{{order.restaurant.wordpress_url}}/wp-admin/post.php?post={{ order.wp_id}}&action=edit" target="_blank">#{{ order.wp_id}} - {{ order.billing.first_name }} {{ order.billing.last_name }} - {{order.get_wp_status_display}}</a></h1> | ||
9 | <div class="grid grid-cols-1 lg:grid-cols-2 md:grid-cols-1 max-h-0 text-center h-full bg-gray-50 text-gray-600 "> | ||
10 | <div class="flex m-10"> | ||
11 | <div class="bg-local block rounded-lg shadow-lg p-10 w-full bg-gray-50"> | ||
12 | <h1 class="text-2xl leading-tight mb-4 font-bold">Zamowienie</h1> | ||
13 | |||
14 | <table class="table-auto w-full"> | ||
15 | <thead> | ||
16 | <tr> | ||
17 | <th>#</th> | ||
18 | <th>Produkt</th> | ||
19 | <th>Cena za produkt</th> | ||
20 | <th>Cena za produkty</th> | ||
21 | <th>Ilosc</th> | ||
22 | </tr> | ||
23 | </thead> | ||
24 | <tbody> | ||
25 | {% for item in order.line_items %} | ||
26 | <tr> | ||
27 | <td>{{ forloop.counter }}</td> | ||
28 | <td>{{ item.name }}</td> | ||
29 | <td>{{item.price}} zl</td> | ||
30 | <td>{{item.subtotal}} zl</td> | ||
31 | <td>{{item.quantity}}</td> | ||
32 | </tr> | ||
33 | {% endfor %} | ||
34 | </tbody> | ||
35 | </table> | ||
36 | |||
37 | <ul class="list-decimal m-2"> | ||
38 | </ul> | ||
39 | </div> | ||
40 | </div> | ||
41 | |||
42 | <div class="flex m-10"> | ||
43 | <div class="bg-local block rounded-lg shadow-lg p-10 w-full bg-gray-50"> | ||
44 | <h1 class="text-2xl leading-tight mb-4 font-bold">Adres dostawy</h1> | ||
45 | <ul class="list-decimal m-2"> | ||
46 | <li class="m-2 w-full text-sm leading-tight font-bold float-left"> | ||
47 | <span class="float-left">Imie i Nazwisko</span> | ||
48 | <span class="float-right">{{order.billing.first_name}} {{order.billing.last_name}}</span> | ||
49 | </li> | ||
50 | <li class="m-2 w-full text-sm leading-tight font-bold float-left"> | ||
51 | <span class="float-left">Numer stolika</span> | ||
52 | <span class="float-right">{{order.billing.address_1}}</span> | ||
53 | </li> | ||
54 | <li class="m-2 w-full text-sm leading-tight font-bold float-left"> | ||
55 | <span class="float-left">Telefon</span> | ||
56 | <span class="float-right">{{order.billing.phone}}</span> | ||
57 | </li> | ||
58 | <li class="m-2 w-full text-sm leading-tight font-bold float-left"> | ||
59 | <span class="float-left">Email</span> | ||
60 | <span class="float-right">{{order.billing.email}}</span> | ||
61 | </li> | ||
62 | </ul> | ||
63 | </div> | ||
64 | </div> | ||
65 | |||
66 | <div class="flex m-10"> | ||
67 | <div class="bg-local block rounded-lg shadow-lg p-10 w-full bg-gray-50"> | ||
68 | <h1 class="text-2xl leading-tight mb-4 font-bold">Doliczenie do zamowienia</h1> | ||
69 | <form method="POST" id="" action="{% url 'dashboard:order_add_to_bill' order.pk %}"> | ||
70 | {% csrf_token %} | ||
71 | {{addToBillForm}} | ||
72 | <input type="submit" value="Dolicz" class="mt-2 inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out"/> | ||
73 | </form> | ||
74 | </div> | ||
75 | </div> | ||
76 | |||
77 | <div class="flex m-10"> | ||
78 | <div class="bg-local block rounded-lg shadow-lg p-10 w-full bg-gray-50"> | ||
79 | <h1 class="text-2xl leading-tight mb-4 font-bold">Akcje</h1> | ||
80 | <form method="POST" id="" action="{% url 'dashboard:order_status_change' order.pk %}"> | ||
81 | {% csrf_token %} | ||
82 | {{orderStatusForm}} | ||
83 | <input type="submit" value="Zapisz" class="ml-2 inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out"/> | ||
84 | </form> | ||
85 | </div> | ||
86 | </div> | ||
87 | </div> | ||
88 | </div> | ||
89 | {% 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 @@ | |||
1 | from django.urls import path | ||
2 | from dashboard.views import Home, DashboardView, DashboardOrderView, ChangeOrderStatusView, AddToBillView | ||
3 | from dashboard.consumers import OrderConsumer | ||
4 | |||
5 | app_name="dashboard" | ||
6 | |||
7 | urlpatterns = [ | ||
8 | path('', Home.as_view(), name="home"), | ||
9 | path('restaurant/<int:restaurant_pk>/', DashboardView.as_view(), name='restaurant_dashboard'), | ||
10 | path('restaurant/order/<int:pk>/', DashboardOrderView.as_view(), name='order_dashboard'), | ||
11 | path('restaurant/order/<int:pk>/change/status/', ChangeOrderStatusView.as_view(), name='order_status_change'), | ||
12 | path('restaurant/order/<int:pk>/add_to_bill/', AddToBillView.as_view(), name='order_add_to_bill'), | ||
13 | ] | ||
14 | |||
15 | websocket_urlpatterns = [ | ||
16 | path('orders/<int:restaurant_pk>/', OrderConsumer.as_asgi()), | ||
17 | ] | ||
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 @@ | |||
1 | from django.shortcuts import render, redirect, reverse | ||
2 | from django.contrib.auth.mixins import LoginRequiredMixin | ||
3 | from django.views.generic.list import ListView, View | ||
4 | from django.views.generic.edit import UpdateView | ||
5 | from django.contrib.messages.views import SuccessMessageMixin | ||
6 | from django.core import serializers | ||
7 | from django.contrib import messages | ||
8 | |||
9 | |||
10 | from dashboard.forms import OrderStatusForm, AddToBillForm | ||
11 | from core.tasks import create_order_and_send_notification | ||
12 | from core.models import Restaurant, Order | ||
13 | |||
14 | class Home(LoginRequiredMixin, View): | ||
15 | def get(self, request): | ||
16 | redirect_url = 'dashboard:restaurant_dashboard' | ||
17 | restaurants = Restaurant.get_user_restaurants(request.user) | ||
18 | |||
19 | if len(restaurants) == 1: | ||
20 | return redirect(redirect_url, restaurant_pk=restaurants[0].pk) | ||
21 | |||
22 | return render(request, template_name='restaurants_choice.html', context={ | ||
23 | 'title': 'Dashboard', | ||
24 | 'restaurants': restaurants, | ||
25 | 'redirect_url': redirect_url | ||
26 | }) | ||
27 | |||
28 | class DashboardView(LoginRequiredMixin, ListView): | ||
29 | template_name = 'dashboard/dashboard.html' | ||
30 | model = Order | ||
31 | paginate_by = 4 | ||
32 | |||
33 | def get_queryset(self, *args, **kwargs): | ||
34 | restaurant = Restaurant.get_user_restaurant_or_404(pk=self.kwargs.get('restaurant_pk'), | ||
35 | user=self.request.user) | ||
36 | |||
37 | status = self.request.GET.get('status') | ||
38 | queryset = {} | ||
39 | if status: | ||
40 | queryset['wp_status'] = status | ||
41 | |||
42 | obj = super().get_queryset(*args, **kwargs).filter( | ||
43 | restaurant=restaurant, | ||
44 | can_display=True, | ||
45 | **queryset | ||
46 | ).order_by('-wp_id') | ||
47 | |||
48 | return obj | ||
49 | |||
50 | |||
51 | class DashboardOrderView(LoginRequiredMixin, View): | ||
52 | def get(self, request, pk): | ||
53 | order = Order.get_order(pk, request.user) | ||
54 | orderStatusForm = OrderStatusForm(instance=order) | ||
55 | addToBillForm = AddToBillForm(pk, request.user) | ||
56 | |||
57 | return render(request, 'dashboard/dashboard_order.html', context={ | ||
58 | 'order': order, | ||
59 | 'orderStatusForm': orderStatusForm, | ||
60 | 'addToBillForm': addToBillForm | ||
61 | }) | ||
62 | |||
63 | |||
64 | class ChangeOrderStatusView(LoginRequiredMixin, SuccessMessageMixin, UpdateView): | ||
65 | form_class = OrderStatusForm | ||
66 | model = Order | ||
67 | success_message = 'Zapisano!' | ||
68 | slug_field='order_pk' | ||
69 | |||
70 | def get_queryset(self, *args, **kwargs): | ||
71 | return super().get_queryset(*args, **kwargs).filter( | ||
72 | pk=self.kwargs['pk'], | ||
73 | can_display=True, | ||
74 | restaurant__users=self.request.user.pk | ||
75 | ) | ||
76 | |||
77 | def get_success_url(self): | ||
78 | return reverse('dashboard:order_dashboard', args=(self.kwargs['pk'], )) | ||
79 | |||
80 | class AddToBillView(LoginRequiredMixin, View): | ||
81 | def post(self, request, pk, *args, **kwargs): | ||
82 | addToBillForm = AddToBillForm(pk, request.user, request.POST) | ||
83 | |||
84 | if addToBillForm.is_valid(): | ||
85 | order = Order.get_order(pk, request.user) | ||
86 | order = serializers.serialize('json', (order, )) | ||
87 | email = True if addToBillForm.data.get('send_mail') else False | ||
88 | phone = True if addToBillForm.data.get('send_sms') else False | ||
89 | items = [(wp_pk, price) for wp_pk, price in request.POST.items() if price.isdigit()] | ||
90 | |||
91 | # TODO: Za duzo tych jebanych argumentow ! | ||
92 | create_order_and_send_notification.delay(order, items, email, phone, request.user.pk) | ||
93 | |||
94 | |||
95 | 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 @@ | |||
1 | #!/usr/bin/env python | ||
2 | """Django's command-line utility for administrative tasks.""" | ||
3 | import os | ||
4 | import sys | ||
5 | |||
6 | |||
7 | def main(): | ||
8 | """Run administrative tasks.""" | ||
9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'restaurant_orders.settings') | ||
10 | try: | ||
11 | from django.core.management import execute_from_command_line | ||
12 | except ImportError as exc: | ||
13 | raise ImportError( | ||
14 | "Couldn't import Django. Are you sure it's installed and " | ||
15 | "available on your PYTHONPATH environment variable? Did you " | ||
16 | "forget to activate a virtual environment?" | ||
17 | ) from exc | ||
18 | execute_from_command_line(sys.argv) | ||
19 | |||
20 | |||
21 | if __name__ == '__main__': | ||
22 | 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 @@ | |||
1 | """ | ||
2 | ASGI config for restaurant_orders project. | ||
3 | |||
4 | It exposes the ASGI callable as a module-level variable named ``application``. | ||
5 | |||
6 | For more information on this file, see | ||
7 | https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ | ||
8 | """ | ||
9 | |||
10 | import os | ||
11 | |||
12 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'restaurant_orders.settings') | ||
13 | import django | ||
14 | django.setup() | ||
15 | |||
16 | from django.core.asgi import get_asgi_application | ||
17 | |||
18 | import restaurant_orders.urls | ||
19 | |||
20 | from channels.auth import AuthMiddlewareStack | ||
21 | from channels.routing import ProtocolTypeRouter, URLRouter | ||
22 | from channels.security.websocket import AllowedHostsOriginValidator | ||
23 | |||
24 | |||
25 | application = ProtocolTypeRouter({ | ||
26 | "http": get_asgi_application(), | ||
27 | "websocket": AllowedHostsOriginValidator( | ||
28 | AuthMiddlewareStack( | ||
29 | URLRouter( | ||
30 | restaurant_orders.urls.websocket_urlpatterns | ||
31 | ) | ||
32 | ) | ||
33 | ), | ||
34 | }) | ||
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 @@ | |||
1 | import os | ||
2 | from celery import Celery | ||
3 | from django.conf import settings | ||
4 | |||
5 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'restaurant_orders.settings') | ||
6 | app = Celery('restaurant_orders') | ||
7 | |||
8 | app.config_from_object('django.conf:settings', namespace='CELERY') | ||
9 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) | ||
10 | |||
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 @@ | |||
1 | import json | ||
2 | |||
3 | from channels.generic.websocket import AsyncWebsocketConsumer | ||
4 | from channels.exceptions import StopConsumer | ||
5 | import channels.layers | ||
6 | |||
7 | from asgiref.sync import sync_to_async, async_to_sync | ||
8 | |||
9 | |||
10 | class NotificationsConsumer(AsyncWebsocketConsumer): | ||
11 | OK = 'ok' | ||
12 | ERROR = 'error' | ||
13 | WARNING = 'warning' | ||
14 | |||
15 | async def connect(self): | ||
16 | self.room_group_name = '' | ||
17 | user = self.scope["user"] | ||
18 | if not user.is_authenticated: | ||
19 | return False | ||
20 | |||
21 | self.room_name = f'user_{user.pk}' | ||
22 | self.room_group_name = f'notifications_{self.room_name}' | ||
23 | |||
24 | # Join room group | ||
25 | await self.channel_layer.group_add( | ||
26 | self.room_group_name, | ||
27 | self.channel_name | ||
28 | ) | ||
29 | |||
30 | await self.accept() | ||
31 | |||
32 | async def disconnect(self, close_code): | ||
33 | raise StopConsumer | ||
34 | |||
35 | async def notify(self, event): | ||
36 | await self.send(event['data']) | ||
37 | |||
38 | @staticmethod | ||
39 | def send_notifications(user_pk, status, message): | ||
40 | layer = channels.layers.get_channel_layer() | ||
41 | |||
42 | async_to_sync(layer.group_send)(f'notifications_user_{user_pk}', { | ||
43 | 'type': 'notify', | ||
44 | 'data': json.dumps({ | ||
45 | 'status': status, | ||
46 | 'message': message | ||
47 | }) | ||
48 | }) | ||
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 @@ | |||
1 | """ | ||
2 | Django settings for restaurant_orders project. | ||
3 | |||
4 | Generated by 'django-admin startproject' using Django 4.0.6. | ||
5 | |||
6 | For more information on this file, see | ||
7 | https://docs.djangoproject.com/en/4.0/topics/settings/ | ||
8 | |||
9 | For the full list of settings and their values, see | ||
10 | https://docs.djangoproject.com/en/4.0/ref/settings/ | ||
11 | """ | ||
12 | |||
13 | from pathlib import Path | ||
14 | import os | ||
15 | |||
16 | # Build paths inside the project like this: BASE_DIR / 'subdir'. | ||
17 | BASE_DIR = Path(__file__).resolve().parent.parent | ||
18 | |||
19 | |||
20 | # Quick-start development settings - unsuitable for production | ||
21 | # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ | ||
22 | |||
23 | # SECURITY WARNING: keep the secret key used in production secret! | ||
24 | SECRET_KEY = os.environ.get('SECRET_KEY') | ||
25 | |||
26 | # SECURITY WARNING: don't run with debug turned on in production! | ||
27 | DEBUG = bool(int(os.environ.get('DEBUG', 0))) | ||
28 | |||
29 | ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',') | ||
30 | |||
31 | LOGIN_URL = 'login' | ||
32 | LOGIN_REDIRECT_URL = 'home' | ||
33 | LOGOUT_REDIRECT_URL = 'home' | ||
34 | |||
35 | REDIS_HOST = os.environ.get('REDIS_HOST') | ||
36 | ASGI_APPLICATION = "restaurant_orders.asgi.application" | ||
37 | CHANNEL_LAYERS = { | ||
38 | 'default': { | ||
39 | 'BACKEND': 'channels_redis.core.RedisChannelLayer', | ||
40 | 'CONFIG': { | ||
41 | "hosts": [(REDIS_HOST, 6379)], | ||
42 | }, | ||
43 | }, | ||
44 | } | ||
45 | |||
46 | # Application definition | ||
47 | |||
48 | INSTALLED_APPS = [ | ||
49 | 'django.contrib.admin', | ||
50 | 'django.contrib.auth', | ||
51 | 'django.contrib.contenttypes', | ||
52 | 'django.contrib.sessions', | ||
53 | 'django.contrib.messages', | ||
54 | 'django.contrib.staticfiles', | ||
55 | 'core', | ||
56 | 'dashboard', | ||
57 | 'wordpress_integration', | ||
58 | 'settings', | ||
59 | 'channels' | ||
60 | ] | ||
61 | |||
62 | |||
63 | MIDDLEWARE = [ | ||
64 | 'django.middleware.security.SecurityMiddleware', | ||
65 | 'django.contrib.sessions.middleware.SessionMiddleware', | ||
66 | 'django.middleware.common.CommonMiddleware', | ||
67 | 'django.middleware.csrf.CsrfViewMiddleware', | ||
68 | 'django.contrib.auth.middleware.AuthenticationMiddleware', | ||
69 | 'django.contrib.messages.middleware.MessageMiddleware', | ||
70 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', | ||
71 | ] | ||
72 | |||
73 | |||
74 | if DEBUG: | ||
75 | INSTALLED_APPS.append("debug_toolbar") | ||
76 | MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware") | ||
77 | |||
78 | INTERNAL_IPS = [ | ||
79 | "127.0.0.1", | ||
80 | ] | ||
81 | |||
82 | |||
83 | ROOT_URLCONF = 'restaurant_orders.urls' | ||
84 | |||
85 | TEMPLATES = [ | ||
86 | { | ||
87 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', | ||
88 | 'DIRS': [BASE_DIR / 'restaurant_orders' / 'templates'], | ||
89 | 'APP_DIRS': True, | ||
90 | 'OPTIONS': { | ||
91 | 'context_processors': [ | ||
92 | 'django.template.context_processors.debug', | ||
93 | 'django.template.context_processors.request', | ||
94 | 'django.contrib.auth.context_processors.auth', | ||
95 | 'django.contrib.messages.context_processors.messages', | ||
96 | ], | ||
97 | }, | ||
98 | }, | ||
99 | ] | ||
100 | |||
101 | WSGI_APPLICATION = 'restaurant_orders.wsgi.application' | ||
102 | |||
103 | |||
104 | # Database | ||
105 | # https://docs.djangoproject.com/en/4.0/ref/settings/#databases | ||
106 | |||
107 | DATABASES = { | ||
108 | 'default': { | ||
109 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', | ||
110 | 'NAME': os.environ.get('DB_NAME'), | ||
111 | 'USER': os.environ.get('DB_USER'), | ||
112 | 'PASSWORD': os.environ.get('DB_PASSWORD'), | ||
113 | 'HOST': os.environ.get('DB_HOST'), | ||
114 | 'PORT': 5432 | ||
115 | } | ||
116 | } | ||
117 | |||
118 | |||
119 | # Password validation | ||
120 | # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators | ||
121 | |||
122 | AUTH_PASSWORD_VALIDATORS = [ | ||
123 | { | ||
124 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', | ||
125 | }, | ||
126 | { | ||
127 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', | ||
128 | }, | ||
129 | { | ||
130 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', | ||
131 | }, | ||
132 | { | ||
133 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', | ||
134 | }, | ||
135 | ] | ||
136 | |||
137 | EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' | ||
138 | EMAIL_HOST = os.environ.get('EMAIL_HOST') | ||
139 | EMAIL_HOST_USER = os.environ.get('EMAIL_USER') | ||
140 | EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_PASSWORD') | ||
141 | EMAIL_PORT = 587 | ||
142 | EMAIL_USE_TLS = True | ||
143 | |||
144 | |||
145 | TWILIO_ACCOUNT_SID = os.environ.get('TWILIO_ACCOUNT_SID') | ||
146 | TWILIO_TOKEN = os.environ.get('TWILIO_TOKEN') | ||
147 | |||
148 | CELERY_BROKER_URL = f"redis://{REDIS_HOST}:6379/0" | ||
149 | CELERY_RESULT_BACKEND = f"redis://{REDIS_HOST}:6379/1" | ||
150 | CELERY_ACCEPT_CONTENT = ['application/json'] | ||
151 | CELERY_TASK_SERIALIZER = 'json' | ||
152 | CELERY_RESULT_SERIALIZER = 'json' | ||
153 | CELERY_TIMEZONE = 'Europe/Warsaw' | ||
154 | |||
155 | # Internationalization | ||
156 | # https://docs.djangoproject.com/en/4.0/topics/i18n/ | ||
157 | |||
158 | LANGUAGE_CODE = 'en-us' | ||
159 | |||
160 | TIME_ZONE = 'UTC' | ||
161 | |||
162 | USE_I18N = True | ||
163 | |||
164 | USE_TZ = True | ||
165 | |||
166 | |||
167 | # Static files (CSS, JavaScript, Images) | ||
168 | # https://docs.djangoproject.com/en/4.0/howto/static-files/ | ||
169 | |||
170 | STATIC_URL = 'static/' | ||
171 | STATIC_ROOT = os.environ.get('STATIC_DIR') | ||
172 | |||
173 | # Default primary key field type | ||
174 | # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field | ||
175 | |||
176 | # DEFAULT_AUTO_FIELD = 'django.db.mod55s.BigAutoField' | ||
177 | 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 @@ | |||
1 | {% if page_obj %} | ||
2 | <script> | ||
3 | function change_page(number) { | ||
4 | const paramsString = window.location.search; | ||
5 | const searchParams = new URLSearchParams(paramsString); | ||
6 | searchParams.set('page', number); | ||
7 | window.location.search = searchParams.toString() | ||
8 | } | ||
9 | </script> | ||
10 | |||
11 | <div class="pagination m-2"> | ||
12 | <span class="pagination__nav"> | ||
13 | {% if page_obj.has_previous %} | ||
14 | <a href="javascript:change_page(1)">« Pierwsza</a> | ||
15 | <a href="javascript:change_page({{ page_obj.previous_page_number }})">Poprzednia</a> | ||
16 | {% endif %} | ||
17 | |||
18 | <span class="current"> | ||
19 | Strona {{ page_obj.number }} z {{ page_obj.paginator.num_pages }}. | ||
20 | </span> | ||
21 | |||
22 | {% if page_obj.has_next %} | ||
23 | <a href="javascript:change_page({{ page_obj.next_page_number }})">Nastepna</a> | ||
24 | <a href="javascript:change_page({{ page_obj.paginator.num_pages }})">Ostatnia »</a> | ||
25 | {% endif %} | ||
26 | </span> | ||
27 | </div> | ||
28 | {% else %} | ||
29 | <div class="m-2"> | ||
30 | <span class="current"> | ||
31 | Strona 0 z 0. | ||
32 | </span> | ||
33 | </div> | ||
34 | {% 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 @@ | |||
1 | <!doctype html> | ||
2 | <html class="no-js" lang="pl"> | ||
3 | <head> | ||
4 | <meta charset="utf-8"> | ||
5 | <meta http-equiv="x-ua-compatible" content="ie=edge"> | ||
6 | <title>{% block title %}{% endblock %}</title> | ||
7 | <meta name="description" content=""> | ||
8 | <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
9 | <script src="https://cdn.tailwindcss.com"></script> | ||
10 | <script src="//cdn.jsdelivr.net/npm/alertifyjs@1.13.1/build/alertify.min.js"></script> | ||
11 | <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/alertifyjs@1.13.1/build/css/alertify.min.css"/> | ||
12 | </head> | ||
13 | <body> | ||
14 | <div id="container" class="flex flex-col h-screen "> | ||
15 | |||
16 | <nav class="relative w-full flex flex-wrap items-center justify-between py-4 bg-gray-100 text-gray-500 hover:text-gray-700 focus:text-gray-700 shadow-lg navbar navbar-expand-lg navbar-light"> | ||
17 | <div class="collapse navbar-collapse flex-grow items-center"> | ||
18 | <a href="{% url 'home' %}" class="ml-4 inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out"> Home </a> | ||
19 | {% if user.is_authenticated %} | ||
20 | <a href="{% url 'dashboard:home' %}" class="ml-4 inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out"> Dashboard </a> | ||
21 | {% if perms.core.change_restaurant %} | ||
22 | <a href="{% url 'settings:home' %}" class="ml-4 inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out"> Ustawienia </a> | ||
23 | {% endif %} | ||
24 | {% endif %} | ||
25 | </div> | ||
26 | {% if user.is_authenticated %} | ||
27 | <a href="{% url 'logout' %}" class="mr-4 inline-block px-6 py-2.5 bg-red-600 text-white font-medium text-xs leading-tight uppercase rounded shadow-md hover:bg-red-700 hover:shadow-lg focus:bg-red-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-red-800 active:shadow-lg transition duration-150 ease-in-out"> Wyloguj sie! </a> | ||
28 | {% else %} | ||
29 | <a href="{% url 'login' %}" class="mr-4 inline-block px-6 py-2.5 bg-green-500 text-white font-medium text-xs leading-tight uppercase rounded shadow-md hover:bg-green-600 hover:shadow-lg focus:bg-green-600 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-green-700 active:shadow-lg transition duration-150 ease-in-out"> Zaloguj sie! </a> | ||
30 | {% endif %} | ||
31 | </nav> | ||
32 | <div id="home" class="flex flex-col text-center h-full bg-gray-200 text-gray-600 "> | ||
33 | {% if messages %} | ||
34 | <div class="grid place-items-center mb-2"> | ||
35 | {% for m in messages %} | ||
36 | {% if m.tags == 'success' %} | ||
37 | <div class="alert bg-green-100 rounded-lg py-5 px-6 mb-3 text-base text-green-700 inline-flex items-center w-full alert-dismissible fade show" role="alert"> | ||
38 | <strong class="mr-1">Udalo sie!</strong> | ||
39 | {% elif m.tags == 'error' %} | ||
40 | <div class="alert bg-red-100 rounded-lg py-5 px-6 mb-3 text-base text-red-700 inline-flex items-center w-full alert-dismissible fade show" role="alert"> | ||
41 | <strong class="mr-1">Niestety wystapil blad!</strong> | ||
42 | {% else %} | ||
43 | <div class="alert bg-yellow-100 rounded-lg py-5 px-6 mb-3 text-base text-yellow-700 inline-flex items-center w-full alert-dismissible fade show" role="alert"> | ||
44 | {% endif %} | ||
45 | {{m}} | ||
46 | <button type="button" class="btn-close box-content w-4 h-4 p-1 ml-auto text-yellow-900 border-none rounded-none opacity-50 focus:shadow-none focus:outline-none focus:opacity-100 hover:text-yellow-900 hover:opacity-75 hover:no-underline" data-bs-dismiss="alert" aria-label="Close"></button> | ||
47 | </div> | ||
48 | {% endfor %} | ||
49 | </div> | ||
50 | {% endif %} | ||
51 | <div class="pt-2"> | ||
52 | {% block content %}{% endblock %} | ||
53 | </div> | ||
54 | </div> | ||
55 | </div> | ||
56 | |||
57 | |||
58 | <script type="text/javascript"> | ||
59 | const notify_socket = new WebSocket( | ||
60 | 'ws://' | ||
61 | + window.location.host | ||
62 | + '/ws/notifications/' | ||
63 | ); | ||
64 | |||
65 | notify_socket.onopen = function(e) { | ||
66 | console.log("[open] Connection established"); | ||
67 | }; | ||
68 | |||
69 | notify_socket.onmessage = function(event) { | ||
70 | let notification = JSON.parse(event.data) | ||
71 | console.log(notification) | ||
72 | |||
73 | switch(notification.status) { | ||
74 | case 'ok': | ||
75 | alertify.notify(`${notification.message}`, 'success', 20); | ||
76 | break; | ||
77 | case 'error': | ||
78 | alertify.notify(`${notification.message}`, 'error', 20); | ||
79 | break; | ||
80 | case 'warning': | ||
81 | alertify.notify(`${notification.message}`, 'warning', 20); | ||
82 | break; | ||
83 | } | ||
84 | }; | ||
85 | notify_socket.onclose = function(e) { | ||
86 | console.error('Chat socket closed unexpectedly'); | ||
87 | }; | ||
88 | </script> | ||
89 | </body> | ||
90 | </html> | ||
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 @@ | |||
1 | {% extends "base.html" %} | ||
2 | |||
3 | {% block title %}Home{% endblock %} | ||
4 | |||
5 | {% block content %} | ||
6 | <div class="py-20"> | ||
7 | <h1 class="text-5xl font-bold mt-0 mb-6 hover:text-gray-900 transition duration-150 ease-in-out"><a href="https://reami.pl" target="_blank">Reami</a></h1> | ||
8 | <h3 class="text-3xl font-bold mb-8">Restaurant orders app</h3> | ||
9 | </div> | ||
10 | {% 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 @@ | |||
1 | {% extends "base.html" %} | ||
2 | |||
3 | {% block title %}Login{% endblock %} | ||
4 | |||
5 | {% block content %} | ||
6 | <div class="grid place-items-center"> | ||
7 | <form method="post" class="p-6 rounded-lg shadow-lg bg-gray-50 justify-center max-w-sm"> | ||
8 | <h1 class="title">Login</h1> | ||
9 | {% csrf_token %} | ||
10 | <div class="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"> | ||
11 | {{form.username.label_tag}} | ||
12 | {{form.username}} | ||
13 | </div> | ||
14 | <div class="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"> | ||
15 | {{form.password.label_tag}} | ||
16 | {{form.password}} | ||
17 | </div> | ||
18 | |||
19 | <input type="submit" value="Login"> | ||
20 | </form> | ||
21 | </div> | ||
22 | {% 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 @@ | |||
1 | {% extends "base.html" %} | ||
2 | |||
3 | {% block title %}Wybierz restauracje{% endblock %} | ||
4 | |||
5 | {% block content %} | ||
6 | {% if restaurants %} | ||
7 | <h3 class="text-4xl font-bold py-20">{{title}}</h3> | ||
8 | <h3 class="text-xl font-bold mb-2">Wybierz restauracje</h3> | ||
9 | <div id="restaurant-choices" class="grid grid-cols-8 max-h-0 text-center h-full bg-gray-50 text-gray-600 "> | ||
10 | {% for restaurant in restaurants %} | ||
11 | <div class="flex m-10"> | ||
12 | <a href="{% url redirect_url restaurant.pk %}"> | ||
13 | <div class="bg-local block rounded-lg shadow-lg bg-gray-100"> | ||
14 | <img class="rounded-t-lg mb-4" src="{{restaurant.logo.url}}" alt="{{restaurant.name}}-logo"/> | ||
15 | <h1 class="text-xl leading-tight pb-2 font-bold">{{restaurant.name}}</h5> | ||
16 | </div> | ||
17 | </a> | ||
18 | </div> | ||
19 | {% endfor %} | ||
20 | </div> | ||
21 | {% else %} | ||
22 | <h3 class="text-3xl font-bold mb-2 py-20">Nie posiadasz restauracji!</h3> | ||
23 | {% endif %} | ||
24 | {% 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 @@ | |||
1 | """restaurant_orders URL Configuration | ||
2 | |||
3 | The `urlpatterns` list routes URLs to views. For more information please see: | ||
4 | https://docs.djangoproject.com/en/4.0/topics/http/urls/ | ||
5 | Examples: | ||
6 | Function views | ||
7 | 1. Add an import: from my_app import views | ||
8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') | ||
9 | Class-based views | ||
10 | 1. Add an import: from other_app.views import Home | ||
11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') | ||
12 | Including another URLconf | ||
13 | 1. Import the include() function: from django.urls import include, path | ||
14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) | ||
15 | """ | ||
16 | from django.contrib import admin | ||
17 | from django.urls import path, include | ||
18 | from django.contrib.auth.views import LoginView, LogoutView | ||
19 | from django.views.generic import TemplateView | ||
20 | from django.conf import settings | ||
21 | from django.conf.urls.static import static | ||
22 | |||
23 | from restaurant_orders.consumers import NotificationsConsumer | ||
24 | |||
25 | from channels.routing import URLRouter | ||
26 | import dashboard.urls | ||
27 | |||
28 | urlpatterns = [ | ||
29 | path('admin/', admin.site.urls), | ||
30 | path('login/', LoginView.as_view(template_name='login.html'), name='login'), | ||
31 | path('logout/', LogoutView.as_view(), name='logout'), | ||
32 | path('dashboard/', include('dashboard.urls')), | ||
33 | path('settings/', include('settings.urls')), | ||
34 | path('webhook/', include('wordpress_integration.urls')), | ||
35 | path('', TemplateView.as_view(template_name='home.html'), name="home"), | ||
36 | ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) | ||
37 | |||
38 | |||
39 | if settings.DEBUG: | ||
40 | urlpatterns.append(path('__debug__/', include('debug_toolbar.urls'))) | ||
41 | |||
42 | websocket_urlpatterns = [ | ||
43 | path('ws/notifications/', NotificationsConsumer.as_asgi()), | ||
44 | path('ws/dashboard/', URLRouter(dashboard.urls.websocket_urlpatterns)), | ||
45 | ] | ||
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 @@ | |||
1 | """ | ||
2 | WSGI config for restaurant_orders project. | ||
3 | |||
4 | It exposes the WSGI callable as a module-level variable named ``application``. | ||
5 | |||
6 | For more information on this file, see | ||
7 | https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ | ||
8 | """ | ||
9 | |||
10 | import os | ||
11 | |||
12 | from django.core.wsgi import get_wsgi_application | ||
13 | |||
14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'restaurant_orders.settings') | ||
15 | |||
16 | 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 --- /dev/null +++ b/restaurant_orders/settings/__init__.py | |||
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 @@ | |||
1 | from django.apps import AppConfig | ||
2 | |||
3 | |||
4 | class SettingsConfig(AppConfig): | ||
5 | default_auto_field = 'django.db.models.BigAutoField' | ||
6 | 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 @@ | |||
1 | from django import forms | ||
2 | from core.models import Restaurant | ||
3 | |||
4 | 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' | ||
5 | |||
6 | class RestaurantForm(forms.ModelForm): | ||
7 | woocommerce_consumer_key = forms.CharField(required=False) | ||
8 | woocommerce_consumer_secret = forms.CharField(required=False) | ||
9 | woocommerce_webhook_secret = forms.CharField(required=False) | ||
10 | |||
11 | class Meta: | ||
12 | model = Restaurant | ||
13 | exclude = ('users', ) | ||
14 | |||
15 | def __init__(self, *args, **kwargs): | ||
16 | super(RestaurantForm, self).__init__(*args, **kwargs) | ||
17 | for key in self.fields.keys(): | ||
18 | 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 @@ | |||
1 | {% extends "base.html" %} | ||
2 | |||
3 | {% block title %}Settings{% endblock %} | ||
4 | |||
5 | {% block content %} | ||
6 | <div class="py-20"> | ||
7 | <h3 class="text-3xl font-bold mb-2">Settings</h3> | ||
8 | |||
9 | <div id='webhook_url' class='w-full mb-8'> | ||
10 | <span>Wpisz ten wehbhook do ustawien w twoim sklepie!</span> | ||
11 | <strong> {{request.scheme}}://{{request.get_host}}{% url 'wordpress_integration:webhook' restaurant_pk=view.kwargs.pk %}</strong> | ||
12 | </div> | ||
13 | |||
14 | <div class="grid place-items-center"> | ||
15 | <form method="post" class="p-6 rounded-lg shadow-lg bg-gray-50 justify-center max-w-sm" enctype="multipart/form-data"> | ||
16 | {% csrf_token %} | ||
17 | {{form.as_p}} | ||
18 | <input type="submit" value="Zatwierdz!" /> | ||
19 | </form> | ||
20 | </div> | ||
21 | </div> | ||
22 | {% 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 @@ | |||
1 | from django.urls import path | ||
2 | from settings.views import Home, RestaurantSettings | ||
3 | |||
4 | app_name = 'settings' | ||
5 | |||
6 | urlpatterns = [ | ||
7 | path('', Home.as_view(), name='home'), | ||
8 | path('restaurant/<int:pk>', RestaurantSettings.as_view(), name='restaurant_settings') | ||
9 | ] | ||
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 @@ | |||
1 | from django.urls import reverse | ||
2 | from django.shortcuts import render, redirect, Http404 | ||
3 | from django.views.generic.edit import UpdateView, CreateView, View | ||
4 | from django.contrib.messages.views import SuccessMessageMixin | ||
5 | from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin | ||
6 | |||
7 | from core.models import Restaurant | ||
8 | from settings.forms import RestaurantForm | ||
9 | |||
10 | |||
11 | class Home(LoginRequiredMixin, PermissionRequiredMixin, View): | ||
12 | permission_required = 'core.change_restaurant' | ||
13 | |||
14 | def get(self, request): | ||
15 | if not self.request.user.has_perm('settings.change_restaurant'): | ||
16 | raise Http404 | ||
17 | |||
18 | redirect_url = 'settings:restaurant_settings' | ||
19 | restaurants = Restaurant.get_user_restaurants(request.user) | ||
20 | |||
21 | if len(restaurants) == 1: | ||
22 | return redirect(redirect_url, pk=restaurants[0].pk) | ||
23 | |||
24 | return render(request, template_name='restaurants_choice.html', context={ | ||
25 | 'title': 'Ustawienia', | ||
26 | 'restaurants': restaurants, | ||
27 | 'redirect_url': redirect_url | ||
28 | }) | ||
29 | |||
30 | class RestaurantSettings(LoginRequiredMixin, SuccessMessageMixin, PermissionRequiredMixin, UpdateView): | ||
31 | template_name = 'settings/restaurant_settings.html' | ||
32 | form_class = RestaurantForm | ||
33 | model = Restaurant | ||
34 | success_message = 'Zapisano!' | ||
35 | permission_required = 'core.change_restaurant' | ||
36 | |||
37 | def get_queryset(self, *args, **kwargs): | ||
38 | return super().get_queryset(*args, **kwargs).filter( | ||
39 | pk=self.kwargs['pk'], | ||
40 | users=self.request.user.pk | ||
41 | ) | ||
42 | |||
43 | def get_success_url(self): | ||
44 | 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 --- /dev/null +++ b/restaurant_orders/wordpress_integration/__init__.py | |||
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 @@ | |||
1 | from django.apps import AppConfig | ||
2 | |||
3 | |||
4 | class WordpressIntegrationConfig(AppConfig): | ||
5 | default_auto_field = 'django.db.models.BigAutoField' | ||
6 | 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 @@ | |||
1 | from wordpress_integration.views import webhook | ||
2 | from django.urls import path | ||
3 | |||
4 | app_name = 'wordpress_integration' | ||
5 | |||
6 | urlpatterns = [ | ||
7 | path('<int:restaurant_pk>/', webhook, name='webhook'), | ||
8 | ] | ||
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 @@ | |||
1 | from core.models import Restaurant, Order | ||
2 | from core.decorators import woocommerce_authentication_required | ||
3 | |||
4 | from django.shortcuts import HttpResponse, get_object_or_404 | ||
5 | from django.views.decorators.csrf import csrf_exempt | ||
6 | from django.views.decorators.http import require_POST | ||
7 | import json | ||
8 | |||
9 | @csrf_exempt | ||
10 | @require_POST | ||
11 | @woocommerce_authentication_required | ||
12 | def webhook(request, restaurant_pk): | ||
13 | payload = request.body.decode('utf-8') | ||
14 | restaurant = get_object_or_404(Restaurant, pk=restaurant_pk) | ||
15 | |||
16 | order = Order.update_or_create_from_response(json.loads(payload), restaurant) | ||
17 | if order is None: | ||
18 | response = HttpResponse('Error, cannot read order properties!') | ||
19 | response.status_code = 400 | ||
20 | return response | ||
21 | |||
22 | return HttpResponse('success') | ||
diff --git a/scripts/celery.sh b/scripts/celery.sh new file mode 100644 index 0000000..aea4be8 --- /dev/null +++ b/scripts/celery.sh | |||
@@ -0,0 +1,5 @@ | |||
1 | #!/bin/sh | ||
2 | |||
3 | set -e | ||
4 | |||
5 | celery -A restaurant_orders.celery worker --loglevel=info | ||
diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh new file mode 100644 index 0000000..b368fd4 --- /dev/null +++ b/scripts/entrypoint.sh | |||
@@ -0,0 +1,9 @@ | |||
1 | #!/bin/sh | ||
2 | |||
3 | set -e | ||
4 | |||
5 | python manage.py collectstatic --noinput | ||
6 | python manage.py makemigrations --noinput | ||
7 | python manage.py migrate --noinput | ||
8 | |||
9 | daphne -b 0.0.0.0 -p 8000 restaurant_orders.asgi:application | ||