summaryrefslogtreecommitdiffstats
path: root/restaurant_orders
diff options
context:
space:
mode:
Diffstat (limited to 'restaurant_orders')
-rw-r--r--restaurant_orders/core/__init__.py0
-rw-r--r--restaurant_orders/core/admin.py5
-rw-r--r--restaurant_orders/core/apps.py6
-rw-r--r--restaurant_orders/core/decorators.py36
-rw-r--r--restaurant_orders/core/models.py90
-rw-r--r--restaurant_orders/core/tasks.py51
-rw-r--r--restaurant_orders/core/utils.py100
-rw-r--r--restaurant_orders/dashboard/__init__.py0
-rw-r--r--restaurant_orders/dashboard/admin.py3
-rw-r--r--restaurant_orders/dashboard/apps.py6
-rw-r--r--restaurant_orders/dashboard/consumers.py49
-rw-r--r--restaurant_orders/dashboard/forms.py30
-rw-r--r--restaurant_orders/dashboard/templates/dashboard/dashboard.html152
-rw-r--r--restaurant_orders/dashboard/templates/dashboard/dashboard_order.html89
-rw-r--r--restaurant_orders/dashboard/urls.py17
-rw-r--r--restaurant_orders/dashboard/views.py95
-rwxr-xr-xrestaurant_orders/manage.py22
-rw-r--r--restaurant_orders/restaurant_orders/__init__.py1
-rw-r--r--restaurant_orders/restaurant_orders/asgi.py34
-rw-r--r--restaurant_orders/restaurant_orders/celery.py10
-rw-r--r--restaurant_orders/restaurant_orders/consumers.py48
-rw-r--r--restaurant_orders/restaurant_orders/settings.py177
-rw-r--r--restaurant_orders/restaurant_orders/templates/_pagination.html34
-rw-r--r--restaurant_orders/restaurant_orders/templates/base.html90
-rw-r--r--restaurant_orders/restaurant_orders/templates/home.html10
-rw-r--r--restaurant_orders/restaurant_orders/templates/login.html22
-rw-r--r--restaurant_orders/restaurant_orders/templates/restaurants_choice.html24
-rw-r--r--restaurant_orders/restaurant_orders/urls.py45
-rw-r--r--restaurant_orders/restaurant_orders/wsgi.py16
-rw-r--r--restaurant_orders/settings/__init__.py0
-rw-r--r--restaurant_orders/settings/admin.py1
-rw-r--r--restaurant_orders/settings/apps.py6
-rw-r--r--restaurant_orders/settings/forms.py18
-rw-r--r--restaurant_orders/settings/templates/settings/restaurant_settings.html22
-rw-r--r--restaurant_orders/settings/urls.py9
-rw-r--r--restaurant_orders/settings/views.py44
-rw-r--r--restaurant_orders/wordpress_integration/__init__.py0
-rw-r--r--restaurant_orders/wordpress_integration/admin.py1
-rw-r--r--restaurant_orders/wordpress_integration/apps.py6
-rw-r--r--restaurant_orders/wordpress_integration/urls.py8
-rw-r--r--restaurant_orders/wordpress_integration/views.py22
41 files changed, 1399 insertions, 0 deletions
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 @@
1from django.contrib import admin
2from core.models import Restaurant, Order
3
4admin.site.register(Restaurant)
5admin.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 @@
1from django.apps import AppConfig
2
3
4class 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 @@
1from django.http import HttpResponse
2from core.models import Restaurant
3
4import base64
5import hashlib
6import hmac
7
8def 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
17def 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
22def 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 @@
1from django.db import models
2from django.contrib.auth.models import User
3from django.shortcuts import Http404, get_object_or_404
4
5from datetime import datetime
6
7
8class 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
29class 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 @@
1from celery import shared_task
2
3from django.core import serializers
4
5from restaurant_orders.consumers import NotificationsConsumer
6
7from core.utils import Orders, SendMail, SendSms
8import re
9
10
11def 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
22def 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 @@
1import json
2from woocommerce import API
3from twilio.rest import Client
4from twilio.base.exceptions import TwilioRestException
5
6from django.core.mail import send_mail
7from django.conf import settings
8
9from core.models import Order
10
11class 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
28class 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
54class 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
68class 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 @@
1from 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 @@
1from django.apps import AppConfig
2
3
4class 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 @@
1from channels.generic.websocket import AsyncWebsocketConsumer
2from channels.exceptions import StopConsumer
3import channels.layers
4
5from asgiref.sync import sync_to_async, async_to_sync
6
7from core.models import Restaurant, Order
8
9from django.db.models import signals
10from django.dispatch import receiver
11from django.core import serializers
12
13
14class 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 @@
1from django import forms
2
3from core.models import Order
4
5
6FORM_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
8class 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
17class 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 @@
1from django.urls import path
2from dashboard.views import Home, DashboardView, DashboardOrderView, ChangeOrderStatusView, AddToBillView
3from dashboard.consumers import OrderConsumer
4
5app_name="dashboard"
6
7urlpatterns = [
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
15websocket_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 @@
1from django.shortcuts import render, redirect, reverse
2from django.contrib.auth.mixins import LoginRequiredMixin
3from django.views.generic.list import ListView, View
4from django.views.generic.edit import UpdateView
5from django.contrib.messages.views import SuccessMessageMixin
6from django.core import serializers
7from django.contrib import messages
8
9
10from dashboard.forms import OrderStatusForm, AddToBillForm
11from core.tasks import create_order_and_send_notification
12from core.models import Restaurant, Order
13
14class 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
28class 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
51class 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
64class 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
80class 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."""
3import os
4import sys
5
6
7def 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
21if __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"""
2ASGI config for restaurant_orders project.
3
4It exposes the ASGI callable as a module-level variable named ``application``.
5
6For more information on this file, see
7https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
8"""
9
10import os
11
12os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'restaurant_orders.settings')
13import django
14django.setup()
15
16from django.core.asgi import get_asgi_application
17
18import restaurant_orders.urls
19
20from channels.auth import AuthMiddlewareStack
21from channels.routing import ProtocolTypeRouter, URLRouter
22from channels.security.websocket import AllowedHostsOriginValidator
23
24
25application = 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 @@
1import os
2from celery import Celery
3from django.conf import settings
4
5os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'restaurant_orders.settings')
6app = Celery('restaurant_orders')
7
8app.config_from_object('django.conf:settings', namespace='CELERY')
9app.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 @@
1import json
2
3from channels.generic.websocket import AsyncWebsocketConsumer
4from channels.exceptions import StopConsumer
5import channels.layers
6
7from asgiref.sync import sync_to_async, async_to_sync
8
9
10class 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"""
2Django settings for restaurant_orders project.
3
4Generated by 'django-admin startproject' using Django 4.0.6.
5
6For more information on this file, see
7https://docs.djangoproject.com/en/4.0/topics/settings/
8
9For the full list of settings and their values, see
10https://docs.djangoproject.com/en/4.0/ref/settings/
11"""
12
13from pathlib import Path
14import os
15
16# Build paths inside the project like this: BASE_DIR / 'subdir'.
17BASE_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!
24SECRET_KEY = os.environ.get('SECRET_KEY')
25
26# SECURITY WARNING: don't run with debug turned on in production!
27DEBUG = bool(int(os.environ.get('DEBUG', 0)))
28
29ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',')
30
31LOGIN_URL = 'login'
32LOGIN_REDIRECT_URL = 'home'
33LOGOUT_REDIRECT_URL = 'home'
34
35REDIS_HOST = os.environ.get('REDIS_HOST')
36ASGI_APPLICATION = "restaurant_orders.asgi.application"
37CHANNEL_LAYERS = {
38 'default': {
39 'BACKEND': 'channels_redis.core.RedisChannelLayer',
40 'CONFIG': {
41 "hosts": [(REDIS_HOST, 6379)],
42 },
43 },
44}
45
46# Application definition
47
48INSTALLED_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
63MIDDLEWARE = [
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
74if 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
83ROOT_URLCONF = 'restaurant_orders.urls'
84
85TEMPLATES = [
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
101WSGI_APPLICATION = 'restaurant_orders.wsgi.application'
102
103
104# Database
105# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
106
107DATABASES = {
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
122AUTH_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
137EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
138EMAIL_HOST = os.environ.get('EMAIL_HOST')
139EMAIL_HOST_USER = os.environ.get('EMAIL_USER')
140EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_PASSWORD')
141EMAIL_PORT = 587
142EMAIL_USE_TLS = True
143
144
145TWILIO_ACCOUNT_SID = os.environ.get('TWILIO_ACCOUNT_SID')
146TWILIO_TOKEN = os.environ.get('TWILIO_TOKEN')
147
148CELERY_BROKER_URL = f"redis://{REDIS_HOST}:6379/0"
149CELERY_RESULT_BACKEND = f"redis://{REDIS_HOST}:6379/1"
150CELERY_ACCEPT_CONTENT = ['application/json']
151CELERY_TASK_SERIALIZER = 'json'
152CELERY_RESULT_SERIALIZER = 'json'
153CELERY_TIMEZONE = 'Europe/Warsaw'
154
155# Internationalization
156# https://docs.djangoproject.com/en/4.0/topics/i18n/
157
158LANGUAGE_CODE = 'en-us'
159
160TIME_ZONE = 'UTC'
161
162USE_I18N = True
163
164USE_TZ = True
165
166
167# Static files (CSS, JavaScript, Images)
168# https://docs.djangoproject.com/en/4.0/howto/static-files/
169
170STATIC_URL = 'static/'
171STATIC_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'
177DEFAULT_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)">&laquo; 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 &raquo;</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
3The `urlpatterns` list routes URLs to views. For more information please see:
4 https://docs.djangoproject.com/en/4.0/topics/http/urls/
5Examples:
6Function views
7 1. Add an import: from my_app import views
8 2. Add a URL to urlpatterns: path('', views.home, name='home')
9Class-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')
12Including 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"""
16from django.contrib import admin
17from django.urls import path, include
18from django.contrib.auth.views import LoginView, LogoutView
19from django.views.generic import TemplateView
20from django.conf import settings
21from django.conf.urls.static import static
22
23from restaurant_orders.consumers import NotificationsConsumer
24
25from channels.routing import URLRouter
26import dashboard.urls
27
28urlpatterns = [
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
39if settings.DEBUG:
40 urlpatterns.append(path('__debug__/', include('debug_toolbar.urls')))
41
42websocket_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"""
2WSGI config for restaurant_orders project.
3
4It exposes the WSGI callable as a module-level variable named ``application``.
5
6For more information on this file, see
7https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
8"""
9
10import os
11
12from django.core.wsgi import get_wsgi_application
13
14os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'restaurant_orders.settings')
15
16application = 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 @@
1from django.apps import AppConfig
2
3
4class 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 @@
1from django import forms
2from core.models import Restaurant
3
4FORM_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
6class 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 @@
1from django.urls import path
2from settings.views import Home, RestaurantSettings
3
4app_name = 'settings'
5
6urlpatterns = [
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 @@
1from django.urls import reverse
2from django.shortcuts import render, redirect, Http404
3from django.views.generic.edit import UpdateView, CreateView, View
4from django.contrib.messages.views import SuccessMessageMixin
5from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
6
7from core.models import Restaurant
8from settings.forms import RestaurantForm
9
10
11class 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
30class 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 @@
1from django.apps import AppConfig
2
3
4class 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 @@
1from wordpress_integration.views import webhook
2from django.urls import path
3
4app_name = 'wordpress_integration'
5
6urlpatterns = [
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 @@
1from core.models import Restaurant, Order
2from core.decorators import woocommerce_authentication_required
3
4from django.shortcuts import HttpResponse, get_object_or_404
5from django.views.decorators.csrf import csrf_exempt
6from django.views.decorators.http import require_POST
7import json
8
9@csrf_exempt
10@require_POST
11@woocommerce_authentication_required
12def 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')