├── delivery ├── __init__.py ├── apps │ ├── __init__.py │ ├── apiv1 │ │ ├── __init__.py │ │ ├── migrations │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── tests.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── urls.py │ │ └── views.py │ └── orders │ │ ├── __init__.py │ │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── rabbitmq_consumer.py │ │ ├── migrations │ │ ├── __init__.py │ │ ├── 0002_auto_20181203_1457.py │ │ ├── 0005_auto_20181204_2208.py │ │ ├── 0006_auto_20181204_2256.py │ │ ├── 0004_taskstate_delivery_person.py │ │ ├── 0003_taskstate.py │ │ └── 0001_initial.py │ │ ├── templatetags │ │ ├── __init__.py │ │ └── utils.py │ │ ├── tests.py │ │ ├── apps.py │ │ ├── urls.py │ │ ├── admin.py │ │ ├── rabbitmq_publisher.py │ │ ├── managers.py │ │ ├── consumers.py │ │ ├── views.py │ │ └── models.py ├── routing.py ├── wsgi.py ├── urls.py └── settings.py ├── README.md ├── templates ├── partials │ ├── messages.html │ └── form-field-material.html ├── index │ ├── task_create_view.html │ ├── task_detail_view.html │ ├── base.html │ ├── delivery-person-dashboard.html │ ├── store-owner-dashboard.html │ └── dashboard.html └── registration │ └── login.html ├── manage.py ├── requirements.txt └── .gitignore /delivery/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /delivery/apps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /delivery/apps/apiv1/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /delivery/apps/orders/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /delivery/apps/apiv1/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /delivery/apps/orders/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /delivery/apps/orders/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /delivery/apps/orders/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /delivery/apps/orders/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /delivery/apps/apiv1/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models 5 | 6 | # Create your models here. 7 | -------------------------------------------------------------------------------- /delivery/apps/apiv1/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.test import TestCase 5 | 6 | # Create your tests here. 7 | -------------------------------------------------------------------------------- /delivery/apps/orders/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.test import TestCase 5 | 6 | # Create your tests here. 7 | -------------------------------------------------------------------------------- /delivery/apps/apiv1/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.contrib import admin 5 | 6 | # Register your models here. 7 | -------------------------------------------------------------------------------- /delivery/apps/apiv1/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.apps import AppConfig 5 | 6 | 7 | class Apiv1Config(AppConfig): 8 | name = 'apiv1' 9 | -------------------------------------------------------------------------------- /delivery/apps/orders/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.apps import AppConfig 5 | 6 | 7 | class OrdersConfig(AppConfig): 8 | name = 'orders' 9 | -------------------------------------------------------------------------------- /delivery/apps/apiv1/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from .views import TaskStateUpdateView 4 | 5 | app_name = 'apiv1' 6 | 7 | urlpatterns = [ 8 | url(r'^tasks/update/$', TaskStateUpdateView.as_view(), name='api-update-task'), 9 | ] 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # deliveryapp 2 | A restaurant delivery management system, with real time updated, implemented using websockets and rabbitmq 3 | For more details please refer to wiki. 4 | 5 | ### Wiki and Screenshots: 6 | [Deliveryapp Wiki](https://github.com/amangarg078/deliveryapp/wiki) 7 | -------------------------------------------------------------------------------- /delivery/routing.py: -------------------------------------------------------------------------------- 1 | from channels import route 2 | from delivery.apps.orders.consumers import ws_connect, ws_disconnect 3 | 4 | channel_routing = [ 5 | # route("http.request", "delivery.apps.orders.consumers.http_consumer"), 6 | route("websocket.connect", ws_connect), 7 | route("websocket.disconnect", ws_disconnect), 8 | ] -------------------------------------------------------------------------------- /templates/partials/messages.html: -------------------------------------------------------------------------------- 1 | {% for message in messages %} 2 |
3 | 7 | 8 | {% endfor %} 9 | -------------------------------------------------------------------------------- /delivery/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for delivery 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/1.11/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", "delivery.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /delivery/apps/orders/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.contrib.auth.decorators import login_required 3 | 4 | from .views import Dashboard, TaskCreateView, TaskDetailView 5 | 6 | app_name = 'orders' 7 | 8 | urlpatterns = [ 9 | url(r'^$', login_required(Dashboard.as_view()), name='dashboard'), 10 | url(r'^tasks/create/$', login_required(TaskCreateView.as_view()), name='task-create-view'), 11 | url(r'^tasks/(?P[0-9A-Fa-f-]+)/$', login_required(TaskDetailView.as_view()), name='task-detail-view'), 12 | ] 13 | -------------------------------------------------------------------------------- /delivery/apps/orders/migrations/0002_auto_20181203_1457.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.16 on 2018-12-03 14:57 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('orders', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='task', 17 | name='priority', 18 | field=models.CharField(choices=[('high', 'High'), ('medium', 'Medium'), ('low', 'Low')], max_length=6), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /delivery/apps/orders/migrations/0005_auto_20181204_2208.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.16 on 2018-12-04 22:08 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('orders', '0004_taskstate_delivery_person'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='task', 17 | name='priority', 18 | field=models.CharField(choices=[('high', 'High'), ('medium', 'Medium'), ('low', 'Low')], default='high', max_length=6), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /delivery/apps/orders/migrations/0006_auto_20181204_2256.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.16 on 2018-12-04 22:56 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('orders', '0005_auto_20181204_2208'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='task', 17 | name='state', 18 | field=models.CharField(choices=[('new', 'New'), ('accepted', 'Accepted'), ('completed', 'Completed'), ('declined', 'Declined'), ('cancelled', 'Cancelled')], default='new', max_length=10), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /delivery/apps/orders/templatetags/utils.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.filter(name='set_element_attributes') 7 | def set_element_attributes(element, attribute_dict): 8 | """ 9 | WARNING!!! This Overrides the previously-set attribute, so use wisely! 10 | TODO: update the attributes instead of overriding 11 | """ 12 | attrs = element.field.widget.attrs 13 | 14 | pairs = attribute_dict.split(',') 15 | 16 | for pair in pairs: 17 | if ':' in pair: 18 | kv = pair.split(':') 19 | attrs[kv[0]] = kv[1] 20 | else: 21 | attrs[pair] = '' 22 | 23 | rendered = str(element) 24 | 25 | return rendered 26 | -------------------------------------------------------------------------------- /delivery/apps/orders/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.contrib import admin 5 | from django.template.defaultfilters import truncatechars # or truncatewords 6 | 7 | from .models import User, Task 8 | 9 | # Register your models here. 10 | 11 | 12 | class TaskAdmin(admin.ModelAdmin): 13 | list_display = ['short_title', 'created_by', 'state', 'priority'] 14 | list_filter = ['created_by', 'assigned_to'] 15 | search_fields = ['created_by__email', 'created_by__username', 'title', 'assigned_to__email', 'assigned_to__username'] 16 | 17 | def short_title(self, obj): 18 | return truncatechars(obj.title, 100) 19 | 20 | 21 | admin.site.register(Task, TaskAdmin) 22 | admin.site.register(User) 23 | -------------------------------------------------------------------------------- /delivery/apps/orders/migrations/0004_taskstate_delivery_person.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.16 on 2018-12-04 16:42 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('orders', '0003_taskstate'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AddField( 18 | model_name='taskstate', 19 | name='delivery_person', 20 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='delivery_person', to=settings.AUTH_USER_MODEL), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /delivery/apps/orders/rabbitmq_publisher.py: -------------------------------------------------------------------------------- 1 | import pika 2 | from django.conf import settings 3 | 4 | 5 | url = settings.RABBITMQ_URL 6 | params = pika.URLParameters(url) 7 | 8 | 9 | class Publisher: 10 | def __init__(self): 11 | self.connection = pika.BlockingConnection(params) # Connect to CloudAMQP 12 | self.channel = self.connection.channel() # start a channel 13 | self.channel.queue_declare(queue=settings.RABBITMQ_QUEUE, arguments={"x-max-priority": 3}) 14 | 15 | def publish_message(self, message, priority): 16 | self.channel.basic_publish(exchange='', routing_key='qprocesstask-3', body=message, properties=pika.BasicProperties(delivery_mode=2, priority=priority)) 17 | print "Message sent to consumer" 18 | self.connection.close() 19 | -------------------------------------------------------------------------------- /delivery/apps/orders/managers.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class TaskManager(models.Manager): 5 | 6 | def get_tasks_for_user(self, user): 7 | from .models import AvailableUserTypes, AvailableTaskStates 8 | 9 | if user.user_type == AvailableUserTypes.store_manager: 10 | return self.filter(created_by=user).order_by('-last_modified_on', '-priority') 11 | 12 | elif user.user_type == AvailableUserTypes.delivery_person: 13 | return self.filter(assigned_to=user).exclude(state__in=[AvailableTaskStates.declined, AvailableTaskStates.cancelled]).order_by('-priority', '-last_modified_on') 14 | return self.all() 15 | 16 | def get_task_by_uid(self, user, task_id): 17 | return self.get_tasks_for_user(user).get(uid=task_id) 18 | -------------------------------------------------------------------------------- /templates/index/task_create_view.html: -------------------------------------------------------------------------------- 1 | {% extends "index/base.html" %} 2 | {% load static %} 3 | {% load utils %} 4 | 5 | 6 | {% block body_classes %}class="text-center bg-light"{% endblock body_classes %} 7 | 8 | {% block body %} 9 | 10 |
11 |
12 |
13 |
14 | {% csrf_token %} 15 | {{form.as_p}} 16 | Back 17 | 18 |
19 |
20 |
21 |
22 | 23 | {% endblock body %} 24 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "delivery.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /delivery/apps/orders/consumers.py: -------------------------------------------------------------------------------- 1 | import json 2 | from channels import Group 3 | from channels.auth import channel_session_user, channel_session_user_from_http 4 | from .models import User 5 | 6 | 7 | @channel_session_user_from_http 8 | def ws_connect(message): 9 | message.reply_channel.send({"accept": True}) 10 | user = message.user 11 | user.get_websocket_group.add(message.reply_channel) 12 | 13 | 14 | @channel_session_user 15 | def ws_disconnect(message): 16 | # Unsubscribe from any connected rooms 17 | for user_id in message.channel_session.get("id", set()): 18 | try: 19 | user = User.objects.get(pk=user_id) 20 | # Removes us from the room's send group. If this doesn't get run, 21 | # we'll get removed once our first reply message expires. 22 | user.websocket_group.discard(message.reply_channel) 23 | except User.DoesNotExist: 24 | pass 25 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appnope==0.1.0 2 | asgi-redis==1.4.3 3 | asgiref==1.1.2 4 | async-timeout==2.0.1 5 | attrs==18.2.0 6 | autobahn==18.11.2 7 | Automat==0.7.0 8 | backports.shutil-get-terminal-size==1.0.0 9 | certifi==2018.10.15 10 | channels==1.1.5 11 | chardet==3.0.4 12 | constantly==15.1.0 13 | daphne==1.4.2 14 | decorator==4.3.0 15 | Django==1.11.29 16 | django-channels==0.7.0 17 | django-rest-framework==0.1.0 18 | djangorestframework==3.9.0 19 | enum34==1.1.6 20 | hyperlink==18.0.0 21 | idna==2.7 22 | incremental==17.5.0 23 | ipdb==0.11 24 | ipython==5.8.0 25 | ipython-genutils==0.2.0 26 | msgpack-python==0.5.6 27 | oauthlib==2.1.0 28 | pathlib==1.0.1 29 | pathlib2==2.3.2 30 | pexpect==4.6.0 31 | pickleshare==0.7.5 32 | pika==0.12.0 33 | prompt-toolkit==1.0.15 34 | ptyprocess==0.6.0 35 | Pygments==2.3.0 36 | PyHamcrest==1.9.0 37 | pytz==2018.7 38 | redis==2.10.6 39 | requests==2.20.1 40 | requests-oauthlib==1.0.0 41 | scandir==1.9.0 42 | simplegeneric==0.8.1 43 | six==1.11.0 44 | traitlets==4.3.2 45 | Twisted==18.9.0 46 | txaio==18.8.1 47 | typing==3.6.6 48 | urllib3==1.24.1 49 | wcwidth==0.1.7 50 | websocket-client==0.54.0 51 | zope.interface==4.6.0 52 | -------------------------------------------------------------------------------- /templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'index/base.html' %} 2 | 3 | {% block title %}Login{% endblock %} 4 | {% block blocksheets %} 5 | 6 | {% endblock blocksheets %} 7 | {% block body_classes %}class="text-center"{% endblock body_classes %} 8 | 9 | {% block body %} 10 | 11 |
12 |

Login

13 | {% csrf_token %} 14 | {% for error in form.non_field_errors %} 15 | 18 | {% endfor %} 19 | 20 | {% include 'partials/form-field-material.html' with field=form.username nolabel=True placeholder="Username" %} 21 | {% include 'partials/form-field-material.html' with field=form.password nolabel=True placeholder="Password"%} 22 | 23 | 24 | 25 |
26 | {% endblock body %} 27 | 28 | -------------------------------------------------------------------------------- /delivery/apps/orders/migrations/0003_taskstate.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.16 on 2018-12-04 16:40 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | import django.utils.timezone 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('orders', '0002_auto_20181203_1457'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='TaskState', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('created_on', models.DateTimeField(default=django.utils.timezone.now, null=True)), 22 | ('last_modified_on', models.DateTimeField(auto_now=True, null=True)), 23 | ('state', models.CharField(choices=[('new', 'New'), ('accepted', 'Accepted'), ('completed', 'Completed'), ('declined', 'Declined'), ('cancelled', 'Cancelled')], default='new', max_length=5)), 24 | ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='orders.Task')), 25 | ], 26 | options={ 27 | 'abstract': False, 28 | }, 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /delivery/urls.py: -------------------------------------------------------------------------------- 1 | """delivery URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.11/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: url(r'^$', 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: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url, include 17 | from django.contrib import admin 18 | from django.contrib.auth import views as auth_views 19 | from delivery.apps.orders import urls as order_urls 20 | from delivery.apps.apiv1 import urls as apiv1_urls 21 | 22 | 23 | 24 | urlpatterns = [ 25 | url(r'^login/$', auth_views.login,{'template_name': 'registration/login.html'}, name='login'), 26 | url(r'^logout/$', auth_views.logout, {'next_page': '/login/'}, name='logout'), 27 | url(r'^admin/', admin.site.urls), 28 | url(r'^', include(order_urls)), 29 | url(r'^apiv1/', include(apiv1_urls)), 30 | ] 31 | -------------------------------------------------------------------------------- /delivery/apps/orders/management/commands/rabbitmq_consumer.py: -------------------------------------------------------------------------------- 1 | import pika 2 | 3 | from django.core.management.base import BaseCommand 4 | from ....orders.models import Task, User, task_manager 5 | from django.utils import timezone 6 | from django.conf import settings 7 | 8 | 9 | # create a function which is called on incoming messages 10 | def callback(ch, method, properties, body): 11 | print body 12 | ack = task_manager(body) 13 | if ack: 14 | ch.basic_ack(delivery_tag=method.delivery_tag) 15 | else: 16 | # if task not assigned to anyone 17 | ch.basic_reject(delivery_tag=method.delivery_tag, requeue=True) 18 | 19 | 20 | class Command(BaseCommand): 21 | """consumer for rabbitmq""" 22 | 23 | def handle(self, *args, **options): 24 | url = settings.RABBITMQ_URL 25 | params = pika.URLParameters(url) 26 | 27 | while True: 28 | connection = pika.BlockingConnection(params) 29 | channel = connection.channel() # start a channel 30 | channel.queue_declare(queue=settings.RABBITMQ_QUEUE, arguments={"x-max-priority": 3}) 31 | 32 | # set up subscription on the queue 33 | channel.basic_consume(callback, queue=settings.RABBITMQ_QUEUE, no_ack=False) 34 | 35 | # start consuming (blocks) 36 | channel.start_consuming() 37 | 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask instance folder 57 | instance/ 58 | 59 | # Sphinx documentation 60 | docs/_build/ 61 | 62 | # PyBuilder 63 | target/ 64 | 65 | # IPython Notebook 66 | .ipynb_checkpoints 67 | 68 | # pyenv 69 | .python-version 70 | 71 | # For pycharm 72 | .idea/ 73 | 74 | *.sqlite3 75 | 76 | # user-added 77 | bower_components/ 78 | node_modules/ 79 | !assets/src/ 80 | !assets/dist/ 81 | !assets/themes/*/src/ 82 | !assets/themes/*/dist/ 83 | media/ 84 | !fab/local_settings.py 85 | .DS_Store 86 | static/ 87 | -------------------------------------------------------------------------------- /templates/index/task_detail_view.html: -------------------------------------------------------------------------------- 1 | {% extends "index/base.html" %} 2 | {% load static %} 3 | {% load utils %} 4 | 5 | 6 | {% block body_classes %}class="bg-light"{% endblock body_classes %} 7 | 8 | {% block body %} 9 | 10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |

Title: {{ object.title }}

18 |

Created on: {{ object.created_on }}

19 |

Last State: {{ object.state }}

20 |
21 | 22 |
23 |
24 |
State trasitions
25 | {% for state in object.get_state_transitions %} 26 |

{{state.created_on}} State: {{state.state}} Assigned to: {{state.delivery_person|default_if_none:"-"}}

27 | {% endfor %} 28 |
29 | Back 30 |
31 |
32 |
33 |
34 |
35 | {% endblock body %} 36 | -------------------------------------------------------------------------------- /templates/partials/form-field-material.html: -------------------------------------------------------------------------------- 1 | {% load utils %} 2 | 35 | -------------------------------------------------------------------------------- /templates/index/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% block meta %} 12 | 13 | 14 | 15 | 16 | {% endblock meta %} 17 | 18 | {% block title %}Delivery{% endblock title %} | Delivery 19 | 20 | 21 | 22 | 23 | Signin Template for Bootstrap 24 | 25 | 26 | 27 | 28 | 29 | {% block stylesheets %} 30 | {% endblock stylesheets %} 31 | 32 | 33 | 34 | 35 | 36 | {% include "partials/messages.html" %} 37 | 38 | {% block body %} 39 |

Hello, world!

40 | {% endblock body %} 41 | 42 | 43 | {% block scripts %} 44 | 45 | {% endblock scripts %} 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /templates/index/delivery-person-dashboard.html: -------------------------------------------------------------------------------- 1 |

Delivery Tasks

2 | 3 |
4 |
5 |
6 |
7 |
8 |
9 |
    10 | {% for object in object_list %} 11 |
  • 12 |
    13 |
    {{ object.title }}
    14 |
    15 | {{ object.priority }} 16 | {{ object.state }} 17 | {% if object.state not in "new,cancelled,completed" %} 18 | 19 | 20 | {% endif %} 21 | {% if object.state == "new" %} 22 | 23 | {% endif %} 24 | 25 |
  • 26 | {% endfor %} 27 |
28 |
29 |
30 |
31 |
32 |
-------------------------------------------------------------------------------- /templates/index/store-owner-dashboard.html: -------------------------------------------------------------------------------- 1 |

Store Manager Tasks

2 |
3 |
4 |
5 | 8 |
9 |
10 | 11 | 12 |
13 |
14 |
15 |
16 |
17 |
18 |
    19 | 20 | {% for object in object_list %} 21 |
  • 22 |
    23 |
    {{ object.title }}
    24 |
    25 | {{ object.priority }} 26 | {{ object.state }} 27 | {{ object.assigned_to|default_if_none:"-" }} 28 | {% if object.state not in "cancelled, accepted, completed" %} 29 | 30 | {% endif %} 31 |
  • 32 | {% endfor %} 33 |
34 |
35 |
36 |
37 | 38 |
39 |
40 | 41 | -------------------------------------------------------------------------------- /delivery/apps/apiv1/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | import json 4 | from rest_framework.response import Response 5 | from rest_framework.views import APIView 6 | from rest_framework import status 7 | from rest_framework.authentication import SessionAuthentication, BasicAuthentication 8 | from rest_framework.permissions import IsAuthenticated 9 | 10 | from ..orders.models import Task, TaskState, AvailableUserTypes, AvailableTaskStates 11 | 12 | 13 | class TaskStateUpdateView(APIView): 14 | authentication_classes = (SessionAuthentication, BasicAuthentication) 15 | permission_classes = (IsAuthenticated,) 16 | 17 | def post(self, request, *args, **kwargs): 18 | payload = request.POST.dict() 19 | user = request.user 20 | uid = payload.get('task') 21 | state = payload.get('state') 22 | try: 23 | task = Task.objects.get(uid=uid) 24 | 25 | except Task.DoesNotExist: 26 | return Response(status=status.HTTP_404_NOT_FOUND) 27 | 28 | user_type = user.user_type 29 | 30 | if user_type == AvailableUserTypes.store_manager: 31 | if state == AvailableTaskStates.cancelled: 32 | task.state = state 33 | task.assigned_to = None 34 | task.save() 35 | return Response(status=status.HTTP_200_OK) 36 | return Response(status=status.HTTP_400_BAD_REQUEST) 37 | 38 | if user_type == AvailableUserTypes.delivery_person: 39 | if state in [AvailableTaskStates.accepted, AvailableTaskStates.completed]: 40 | task.state = AvailableTaskStates.new 41 | task.assigned_to = user 42 | task.save() 43 | return Response(status=status.HTTP_200_OK) 44 | if state == AvailableTaskStates.declined: 45 | task.state = state 46 | task.assigned_to = None 47 | task.save() 48 | return Response(status=status.HTTP_200_OK) 49 | return Response(status=status.HTTP_400_BAD_REQUEST) 50 | 51 | return Response(status=status.HTTP_200_OK) 52 | -------------------------------------------------------------------------------- /delivery/apps/orders/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.utils import dateparse, timezone 5 | from django.utils.dateparse import parse_datetime 6 | from django.shortcuts import render, get_object_or_404 7 | from django.http import HttpResponse, HttpResponseRedirect 8 | from django.views.generic import TemplateView, DetailView, RedirectView 9 | from django.views.generic.list import ListView 10 | from django.views.generic.edit import CreateView, DeleteView, UpdateView, FormView 11 | from django.core.urlresolvers import reverse 12 | from django.contrib import messages 13 | 14 | from django.conf import settings 15 | 16 | from .models import * 17 | 18 | 19 | class GetTaskObjectMixin(object): 20 | def get_object(self, *args, **kwargs): 21 | self.object = None 22 | task_id = self.kwargs.get('uid', None) 23 | if task_id: 24 | try: 25 | task = Task.objects.get_task_by_uid(self.request.user, task_id) 26 | except Task.DoesNotExist: 27 | raise Http404('No Task matches the given query.') 28 | self.object = task 29 | return task 30 | else: 31 | return None 32 | 33 | 34 | class Dashboard(ListView): 35 | model = Task 36 | template_name = 'index/dashboard.html' 37 | 38 | def get_queryset(self, *args, **kwargs): 39 | user = self.request.user 40 | return self.model.objects.get_tasks_for_user(user) 41 | 42 | 43 | class TaskCreateView(CreateView): 44 | template_name = "index/task_create_view.html" 45 | model = Task 46 | fields = ['title', 'priority'] 47 | 48 | def form_valid(self, form): 49 | obj = form.save(commit=False) 50 | # Task form is only available to store owner 51 | obj.created_by = self.request.user 52 | obj.save() 53 | self.object = obj 54 | return super(TaskCreateView, self).form_valid(form) 55 | 56 | def get_success_url(self): 57 | return reverse('orders:dashboard') 58 | 59 | 60 | class TaskDetailView(GetTaskObjectMixin, DetailView): 61 | model = Task 62 | template_name = "index/task_detail_view.html" 63 | 64 | -------------------------------------------------------------------------------- /delivery/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for delivery project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.11.15. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.11/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '%d%3xt4v$#hhy*+lf6x(5z_$$geh@!qs2m_!ko@42dbobhqk*4' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 41 | # Third party apps 42 | 'channels', 43 | 'rest_framework', 44 | 45 | # our apps 46 | 'delivery.apps.orders', 47 | 'delivery.apps.apiv1', 48 | ] 49 | 50 | MIDDLEWARE = [ 51 | 'django.middleware.security.SecurityMiddleware', 52 | 'django.contrib.sessions.middleware.SessionMiddleware', 53 | 'django.middleware.common.CommonMiddleware', 54 | 'django.middleware.csrf.CsrfViewMiddleware', 55 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 56 | 'django.contrib.messages.middleware.MessageMiddleware', 57 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 58 | ] 59 | 60 | ROOT_URLCONF = 'delivery.urls' 61 | 62 | TEMPLATES = [ 63 | { 64 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 65 | 'DIRS': [os.path.join(BASE_DIR, 'templates')], 66 | 'APP_DIRS': True, 67 | 'OPTIONS': { 68 | 'context_processors': [ 69 | 'django.template.context_processors.debug', 70 | 'django.template.context_processors.request', 71 | 'django.contrib.auth.context_processors.auth', 72 | 'django.contrib.messages.context_processors.messages', 73 | ], 74 | }, 75 | }, 76 | ] 77 | 78 | WSGI_APPLICATION = 'delivery.wsgi.application' 79 | 80 | 81 | # Database 82 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 83 | 84 | DATABASES = { 85 | 'default': { 86 | 'ENGINE': 'django.db.backends.sqlite3', 87 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 88 | } 89 | } 90 | 91 | 92 | # Password validation 93 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 94 | 95 | AUTH_PASSWORD_VALIDATORS = [ 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 101 | }, 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 104 | }, 105 | { 106 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 107 | }, 108 | ] 109 | 110 | 111 | # Internationalization 112 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 113 | 114 | LANGUAGE_CODE = 'en-us' 115 | 116 | TIME_ZONE = 'UTC' 117 | 118 | USE_I18N = True 119 | 120 | USE_L10N = True 121 | 122 | USE_TZ = True 123 | 124 | 125 | # Static files (CSS, JavaScript, Images) 126 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 127 | 128 | STATIC_URL = '/static/' 129 | 130 | AUTH_USER_MODEL = 'orders.User' 131 | LOGIN_REDIRECT_URL = 'orders:dashboard' 132 | LOGIN_URL = '/login/' 133 | 134 | redis_host = os.environ.get('REDIS_HOST', 'localhost') 135 | 136 | 137 | CHANNEL_LAYERS = { 138 | "default": { 139 | "BACKEND": "asgi_redis.RedisChannelLayer", 140 | "CONFIG": { 141 | "hosts": [os.environ.get('REDIS_URL', 'redis://localhost:6379')], 142 | }, 143 | "ROUTING": "delivery.routing.channel_routing", 144 | }, 145 | } 146 | 147 | RABBITMQ_URL = "amqp://guest:guest@localhost:5672/%2F" 148 | RABBITMQ_QUEUE = "qprocesstask-3" 149 | -------------------------------------------------------------------------------- /delivery/apps/orders/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.16 on 2018-11-28 17:31 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | import django.contrib.auth.models 7 | import django.contrib.auth.validators 8 | from django.db import migrations, models 9 | import django.db.models.deletion 10 | import django.utils.timezone 11 | import uuid 12 | 13 | 14 | class Migration(migrations.Migration): 15 | 16 | initial = True 17 | 18 | dependencies = [ 19 | ('auth', '0008_alter_user_username_max_length'), 20 | ] 21 | 22 | operations = [ 23 | migrations.CreateModel( 24 | name='User', 25 | fields=[ 26 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 27 | ('password', models.CharField(max_length=128, verbose_name='password')), 28 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 29 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 30 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.ASCIIUsernameValidator()], verbose_name='username')), 31 | ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), 32 | ('last_name', models.CharField(blank=True, max_length=30, verbose_name='last name')), 33 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), 34 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 35 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 36 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 37 | ('uid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), 38 | ('user_type', models.CharField(blank=True, choices=[('SM', 'Store Manager'), ('DP', 'Delivery Person')], max_length=2, null=True)), 39 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), 40 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), 41 | ], 42 | options={ 43 | 'abstract': False, 44 | 'verbose_name': 'user', 45 | 'verbose_name_plural': 'users', 46 | }, 47 | managers=[ 48 | ('objects', django.contrib.auth.models.UserManager()), 49 | ], 50 | ), 51 | migrations.CreateModel( 52 | name='Task', 53 | fields=[ 54 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 55 | ('created_on', models.DateTimeField(default=django.utils.timezone.now, null=True)), 56 | ('last_modified_on', models.DateTimeField(auto_now=True, null=True)), 57 | ('uid', models.UUIDField(default=uuid.uuid4, unique=True)), 58 | ('title', models.CharField(max_length=512)), 59 | ('priority', models.CharField(choices=[('high', 'High'), ('medium', 'Medium'), ('low', 'Low')], max_length=5)), 60 | ('state', models.CharField(choices=[('new', 'New'), ('accepted', 'Accepted'), ('completed', 'Completed'), ('declined', 'Declined'), ('cancelled', 'Cancelled')], default='new', max_length=5)), 61 | ('assigned_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='assigned_to', to=settings.AUTH_USER_MODEL)), 62 | ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_by', to=settings.AUTH_USER_MODEL)), 63 | ], 64 | options={ 65 | 'abstract': False, 66 | }, 67 | ), 68 | ] 69 | -------------------------------------------------------------------------------- /templates/index/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends "index/base.html" %} 2 | {% load static %} 3 | {% load utils %} 4 | 5 | 6 | {% block body_classes %}class="text-center bg-light"{% endblock body_classes %} 7 | 8 | {% block body %} 9 | 10 | 11 |
12 |
13 |
    14 | {% for user in users %} 15 | 16 |
  • 17 | {{ user.username|escape }}: {{ user.status|default:'Offline' }} 18 |
  • 19 | {% endfor %} 20 |
21 | {% if request.user.is_store_manager %} 22 | {% include "index/store-owner-dashboard.html" with object_list=object_list %} 23 | {% else %} 24 | {% include "index/delivery-person-dashboard.html" with object_list=object_list %} 25 | {% endif %} 26 |
27 |
28 | 29 | {% endblock body %} 30 | 31 | 32 | {% block scripts %} 33 | 108 | 123 | {% endblock scripts %} -------------------------------------------------------------------------------- /delivery/apps/orders/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | import uuid 4 | import json 5 | from channels import Group 6 | 7 | from django.db import models 8 | from django.contrib.auth.models import AbstractUser 9 | from django.utils import timezone 10 | from django.db.models.signals import post_save, post_delete 11 | from django.dispatch import receiver 12 | from django.db.models import Max, Count, IntegerField, Case, When, Q, F, OuterRef, Subquery 13 | 14 | 15 | from .managers import TaskManager 16 | from .rabbitmq_publisher import Publisher 17 | 18 | 19 | class AvailableUserTypes(object): 20 | store_manager = "SM" 21 | delivery_person = "DP" 22 | 23 | 24 | CHOICES_USER_TYPE = ( 25 | (AvailableUserTypes.store_manager, "Store Manager"), 26 | (AvailableUserTypes.delivery_person, "Delivery Person") 27 | ) 28 | 29 | 30 | class AvailableTaskPriority(object): 31 | high = "high" 32 | medium = "medium" 33 | low = "low" 34 | 35 | 36 | PRIORITY_MAPPING = { 37 | AvailableTaskPriority.high: 3, 38 | AvailableTaskPriority.medium: 2, 39 | AvailableTaskPriority.low: 1, 40 | } 41 | 42 | 43 | CHOICES_TASK_PRIORITY = ( 44 | (AvailableTaskPriority.high, "High"), 45 | (AvailableTaskPriority.medium, "Medium"), 46 | (AvailableTaskPriority.low, "Low"), 47 | ) 48 | 49 | 50 | class AvailableTaskStates(object): 51 | new = "new" 52 | accepted = "accepted" 53 | completed = "completed" 54 | declined = "declined" 55 | cancelled = "cancelled" 56 | 57 | 58 | CHOICES_TASK_STATE = ( 59 | (AvailableTaskStates.new, "New"), 60 | (AvailableTaskStates.accepted, "Accepted"), 61 | (AvailableTaskStates.completed, "Completed"), 62 | (AvailableTaskStates.declined, "Declined"), 63 | (AvailableTaskStates.cancelled, "Cancelled"), 64 | ) 65 | 66 | 67 | class User(AbstractUser): 68 | uid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False) 69 | user_type = models.CharField(max_length=2, null=True, blank=True, choices=CHOICES_USER_TYPE) 70 | 71 | def is_store_manager(self): 72 | if self.user_type == AvailableUserTypes.store_manager: 73 | return True 74 | return False 75 | 76 | @property 77 | def get_websocket_group(self): 78 | """ 79 | Returns the Channels Group that sockets should subscribe to to get sent 80 | messages as they are generated. 81 | """ 82 | return Group("user-%s" % self.uid) 83 | 84 | 85 | class BaseModel(models.Model): 86 | created_on = models.DateTimeField(null=True, default=timezone.now) 87 | last_modified_on = models.DateTimeField(null=True, auto_now=True) 88 | 89 | class Meta: 90 | abstract = True 91 | 92 | 93 | class Task(BaseModel): 94 | uid = models.UUIDField(default=uuid.uuid4, unique=True) 95 | title = models.CharField(max_length=512) 96 | priority = models.CharField(max_length=6, choices=CHOICES_TASK_PRIORITY, default=AvailableTaskPriority.high) 97 | state = models.CharField(max_length=10, choices=CHOICES_TASK_STATE, default=AvailableTaskStates.new) 98 | created_by = models.ForeignKey(User, related_name="created_by") 99 | assigned_to = models.ForeignKey(User, related_name="assigned_to", null=True, blank=True) 100 | 101 | objects = TaskManager() 102 | 103 | def __unicode__(self): 104 | return self.title 105 | 106 | def get_state_transitions(self): 107 | return self.taskstate_set.all().order_by('-created_on') 108 | 109 | def get_message_content(self): 110 | data = {} 111 | data['task_id'] = str(self.uid) 112 | data['title'] = self.title 113 | data['state'] = self.state 114 | data['assigned_to'] = self.assigned_to.username if self.assigned_to else "-" 115 | data['priority'] = self.priority 116 | return data 117 | 118 | 119 | class TaskState(BaseModel): 120 | task = models.ForeignKey(Task) 121 | state = models.CharField(max_length=5, choices=CHOICES_TASK_STATE, default=AvailableTaskStates.new) 122 | delivery_person = models.ForeignKey(User, related_name="delivery_person", null=True, blank=True) 123 | 124 | def __unicode__(self): 125 | return self.task.title 126 | 127 | 128 | def task_manager(body): 129 | body_dict = json.loads(body) 130 | try: 131 | task = Task.objects.get(uid=body_dict.get('task_id')) 132 | available_delivery_persons = User.objects.filter(user_type=AvailableUserTypes.delivery_person).annotate(assinged_count=Count(Case(When(assigned_to__state=AvailableTaskStates.accepted,then=1)))).filter(assinged_count__lte=3) 133 | assigned_to = available_delivery_persons.first() #routing logic to come here 134 | if assigned_to: 135 | task.assigned_to = assigned_to 136 | task.save() 137 | return True 138 | except Task.DoesNotExist: 139 | pass 140 | return False 141 | 142 | 143 | @receiver(post_save, sender=Task, dispatch_uid='task_post_save_handler') 144 | def task_post_save_handler(sender, instance, created, using, **kwargs): 145 | if created: 146 | TaskState.objects.create(task=instance) 147 | else: 148 | last_task_state = TaskState.objects.filter(task=instance).order_by('-created_on').first() 149 | if last_task_state.state != instance.state or last_task_state.delivery_person != instance.assigned_to: 150 | TaskState.objects.create(task=instance, state=instance.state, delivery_person=instance.assigned_to) 151 | 152 | message_content_store_owner = instance.get_message_content() 153 | message_content_delivery_person = message_content_store_owner.copy() 154 | 155 | store_manager = instance.created_by 156 | message_content_store_owner['user_type'] = store_manager.user_type 157 | 158 | store_manager.get_websocket_group.send({ 159 | 'text': json.dumps(message_content_store_owner) 160 | }) 161 | 162 | delivery_person = instance.assigned_to 163 | if delivery_person: 164 | message_content_delivery_person['user_type'] = delivery_person.user_type 165 | 166 | if instance.state in [AvailableTaskStates.declined, AvailableTaskStates.cancelled]: 167 | message_content_delivery_person['action'] = "remove" 168 | if instance.state == AvailableTaskStates.accepted: 169 | message_content_delivery_person['action'] = "accepted_links" 170 | delivery_person.get_websocket_group.send({ 171 | 'text': json.dumps(message_content_delivery_person) 172 | }) 173 | 174 | if instance.state in [AvailableTaskStates.new, AvailableTaskStates.declined] and not instance.assigned_to: 175 | msg = json.dumps({'task_id': str(instance.uid)}) 176 | Publisher().publish_message(msg, PRIORITY_MAPPING.get(instance.priority)) 177 | 178 | --------------------------------------------------------------------------------