├── .gitignore ├── Procfile ├── README.md ├── _config.yml ├── demo ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py ├── foodtaskerapp ├── .DS_Store ├── __init__.py ├── admin.py ├── apis.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_customer_driver.py │ ├── 0003_meal.py │ ├── 0004_order.py │ ├── 0005_orderdetails.py │ ├── 0006_auto_20180616_0927.py │ ├── 0007_driver_location.py │ └── __init__.py ├── models.py ├── serializers.py ├── social_auth_pipeline.py ├── static │ ├── .DS_Store │ ├── css │ │ ├── .DS_Store │ │ ├── bootstrap.min.css │ │ ├── bootstrap.min.css.map │ │ └── style.css │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ ├── image │ │ ├── McDonalds.png │ │ ├── burger.jpeg │ │ ├── restaurant.jpeg │ │ └── tamjaisamgor.jpg │ └── js │ │ ├── .DS_Store │ │ └── bootstrap.min.js ├── templates │ ├── base.html │ ├── base_signup.html │ └── restaurant │ │ ├── account.html │ │ ├── add_meal.html │ │ ├── base.html │ │ ├── edit_meal.html │ │ ├── meal.html │ │ ├── order.html │ │ ├── report.html │ │ ├── sign_in.html │ │ └── sign_up.html ├── tests.py └── views.py ├── manage.py ├── media ├── meal_images │ ├── burger.jpeg │ ├── burger_4yAvu5x.jpeg │ └── tutorial_-74.jpg └── restaurant_logo │ ├── 2C.jpg │ ├── 2C_o5bAwpp.jpg │ ├── 2C_tmf7CLd.jpg │ ├── McDonalds.png │ ├── _DSC1320v2.jpg │ ├── restaurant.jpeg │ └── tamjaisamgor.jpg ├── requirements.txt └── runtime.txt /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | db.sqlite3 3 | *.pyc 4 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn demo.wsgi --log-file - 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Foodtasker-django 2 | A comprehensive system for Food Delivery like UberEats, Doordash and Postmate. This is a course project for "Create UberEats with Python/Django and Swift 3" in Code4Startup. I extended some functions based on the existing project. 3 | 4 | ## Introduction 5 | 6 | **FoodTasker** is an Web Application Dashboard for restaurants to register, create Meals, monitor Orders and manage Transactions. Its main features are: 7 | * Super Admin Dashboard to handle everything in the system 8 | 9 | * Sign in/Sign out process for Restaurants 10 | 11 | * Hosting the web app on Heroku 12 | 13 | * Page for Account information 14 | 15 | * Page for Restaurant to upload images and create Menu 16 | 17 | * Page for managing all Orders from Customer 18 | 19 | * Real-time notification when new orders coming in 20 | 21 | * Statistic bar charts and pie charts 22 | 23 | * Process to listen and response API requests from clients 24 | 25 | * Visa & Credit card process with Stripe 26 | 27 | * Real-time updating Drivers's location on the Map 28 | 29 | ## Credits 30 | 31 | leotrieu 32 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /demo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattchw/Foodtasker-django/184c4a776236dfe189b5edc922616faacc2c3244/demo/__init__.py -------------------------------------------------------------------------------- /demo/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for demo project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.10. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.10/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.10/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'j5k0!6v8o$&vtun#y@@43(jd5hb--&sd^619hvppn^l-03imcq' 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 | 'foodtaskerapp', 41 | 'oauth2_provider', 42 | 'social.apps.django_app.default', 43 | 'rest_framework_social_oauth2', 44 | 'bootstrap3' 45 | ] 46 | 47 | MIDDLEWARE = [ 48 | 'django.middleware.security.SecurityMiddleware', 49 | 'django.contrib.sessions.middleware.SessionMiddleware', 50 | 'django.middleware.common.CommonMiddleware', 51 | 'django.middleware.csrf.CsrfViewMiddleware', 52 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 53 | 'django.contrib.messages.middleware.MessageMiddleware', 54 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 55 | ] 56 | 57 | ROOT_URLCONF = 'demo.urls' 58 | 59 | TEMPLATES = [ 60 | { 61 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 62 | 'DIRS': [], 63 | 'APP_DIRS': True, 64 | 'OPTIONS': { 65 | 'context_processors': [ 66 | 'django.template.context_processors.debug', 67 | 'django.template.context_processors.request', 68 | 'django.contrib.auth.context_processors.auth', 69 | 'django.contrib.messages.context_processors.messages', 70 | 'django.template.context_processors.media', 71 | 'social_django.context_processors.backends', 72 | 'social_django.context_processors.login_redirect', 73 | ], 74 | }, 75 | }, 76 | ] 77 | 78 | WSGI_APPLICATION = 'demo.wsgi.application' 79 | 80 | 81 | # Database 82 | # https://docs.djangoproject.com/en/1.10/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.10/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.10/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.10/howto/static-files/ 127 | 128 | STATIC_URL = '/static/' 129 | STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') 130 | 131 | LOGIN_REDIRECT_URL = '/' 132 | 133 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 134 | MEDIA_URL = '/media/' 135 | 136 | import dj_database_url 137 | db_from_env = dj_database_url.config() 138 | DATABASES['default'].update(db_from_env) 139 | 140 | AUTHENTICATION_BACKENDS = ( 141 | 'social.backends.facebook.FacebookOAuth2', 142 | #'rest_framework_social_oauth2.backends.DjangoOAuth2', 143 | 'django.contrib.auth.backends.ModelBackend', 144 | ) 145 | 146 | # Facebook configuration 147 | SOCIAL_AUTH_FACEBOOK_KEY = '231332064296911' 148 | SOCIAL_AUTH_FACEBOOK_SECRET = '1003aee47ecd62cbe5a7b48bd2c2de77' 149 | 150 | # Define SOCIAL_AUTH_FACEBOOK_SCOPE to get extra permissions from facebook. Email is not sent by default, to get it, you must request the email permission: 151 | SOCIAL_AUTH_FACEBOOK_SCOPE = ['email'] 152 | SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS = { 153 | 'fields': 'id, name, email' 154 | } 155 | 156 | SOCIAL_AUTH_PIPELINE = ( 157 | 'social_core.pipeline.social_auth.social_details', 158 | 'social_core.pipeline.social_auth.social_uid', 159 | 'social_core.pipeline.social_auth.auth_allowed', 160 | 'social_core.pipeline.social_auth.social_user', 161 | 'social_core.pipeline.user.get_username', 162 | 'social_core.pipeline.user.create_user', 163 | 'foodtaskerapp.social_auth_pipeline.create_user_by_type', # <--- set the path to the function 164 | 'social_core.pipeline.social_auth.associate_user', 165 | 'social_core.pipeline.social_auth.load_extra_data', 166 | 'social_core.pipeline.user.user_details', 167 | ) 168 | 169 | STRIPE_API_KEY = 'sk_test_SumJFLeKUVT9OlzsESPaTxiS' 170 | -------------------------------------------------------------------------------- /demo/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url, include 2 | from django.contrib import admin 3 | from django.contrib.auth import views as auth_views 4 | from django.conf.urls.static import static 5 | from django.conf import settings 6 | 7 | from foodtaskerapp import views, apis 8 | 9 | urlpatterns = [ 10 | url(r'^admin/', admin.site.urls), 11 | url(r'^$', views.home, name='home'), 12 | url(r'^restaurant/sign-in/$', auth_views.login, {'template_name': 'restaurant/sign_in.html'}, name='restaurant-sign-in'), 13 | url(r'^restaurant/sign-out/$', auth_views.logout, {'next_page': '/'}, name='restaurant-sign-out'), 14 | url(r'^restaurant/sign-up/$', views.restaurant_sign_up, name='restaurant-sign-up'), 15 | url(r'^restaurant/$', views.restaurant_home, name='restaurant-home'), 16 | 17 | url(r'^restaurant/account/$', views.restaurant_account, name='restaurant-account'), 18 | url(r'^restaurant/meal/$', views.restaurant_meal, name='restaurant-meal'), 19 | url(r'^restaurant/meal/add/$', views.restaurant_add_meal, name='restaurant-add-meal'), 20 | url(r'^restaurant/meal/edit/(?P\d+)/$', views.restaurant_edit_meal, name='restaurant-edit-meal'), 21 | url(r'^restaurant/order/$', views.restaurant_order, name='restaurant-order'), 22 | url(r'^restaurant/report/$', views.restaurant_report, name='restaurant-report'), 23 | 24 | # Sign in/Sign up/Sign out 25 | url(r'^api/social/', include('rest_framework_social_oauth2.urls')), 26 | # convert-token (sign in/sign up) 27 | # revoke-token (sign out) 28 | url(r'^api/restaurant/order/notification/(?P.+)/$', apis.restaurant_order_notification), 29 | 30 | # APIs for customers 31 | url(r'^api/customer/restaurants/$', apis.customer_get_restaurants), 32 | url(r'^api/customer/meals/(?P\d+)/$', apis.customer_get_meals), 33 | url(r'^api/customer/order/add/$', apis.customer_add_order), 34 | url(r'^api/customer/order/latest/$', apis.customer_get_latest_order), 35 | url(r'^api/customer/driver/location/$', apis.customer_driver_location), 36 | 37 | # APIs for drivers 38 | url(r'^api/driver/orders/ready/$', apis.driver_get_ready_orders), 39 | url(r'^api/driver/order/pick/$', apis.driver_pick_order), 40 | url(r'^api/driver/order/latest/$', apis.driver_get_latest_order), 41 | url(r'^api/driver/order/complete/$', apis.driver_complete_order), 42 | url(r'^api/driver/revenue/$', apis.driver_get_revenue), 43 | url(r'^api/driver/location/update/$', apis.driver_update_location), 44 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 45 | -------------------------------------------------------------------------------- /demo/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for demo 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.10/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", "demo.settings") 15 | 16 | application = get_wsgi_application() 17 | 18 | # Use whitenoise package to serve static files on Heroku 19 | from whitenoise.django import DjangoWhiteNoise 20 | application = DjangoWhiteNoise(application) 21 | -------------------------------------------------------------------------------- /foodtaskerapp/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattchw/Foodtasker-django/184c4a776236dfe189b5edc922616faacc2c3244/foodtaskerapp/.DS_Store -------------------------------------------------------------------------------- /foodtaskerapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattchw/Foodtasker-django/184c4a776236dfe189b5edc922616faacc2c3244/foodtaskerapp/__init__.py -------------------------------------------------------------------------------- /foodtaskerapp/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | from foodtaskerapp.models import Restaurant, Customer, Driver, Meal, Order, OrderDetails 5 | 6 | admin.site.register(Restaurant) 7 | admin.site.register(Customer) 8 | admin.site.register(Driver) 9 | admin.site.register(Meal) 10 | admin.site.register(Order) 11 | admin.site.register(OrderDetails) 12 | -------------------------------------------------------------------------------- /foodtaskerapp/apis.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.utils import timezone 4 | from django.http import JsonResponse 5 | from django.views.decorators.csrf import csrf_exempt 6 | 7 | from oauth2_provider.models import AccessToken 8 | 9 | from foodtaskerapp.models import Restaurant, Driver, Meal, Order, OrderDetails 10 | from foodtaskerapp.serializers import RestaurantSerializer, MealSerializer, OrderSerializer 11 | 12 | import stripe 13 | from demo.settings import STRIPE_API_KEY 14 | 15 | stripe.api_key = STRIPE_API_KEY 16 | 17 | ############# 18 | # CUSTOMERS # 19 | ############# 20 | 21 | def customer_get_restaurants(request): 22 | restaurants = RestaurantSerializer( 23 | Restaurant.objects.all().order_by("-id"), 24 | many = True, 25 | context = {"request":request} 26 | ).data 27 | 28 | return JsonResponse({"restaurants": restaurants}) 29 | 30 | def customer_get_meals(request, restaurant_id): 31 | meals = MealSerializer( 32 | Meal.objects.filter(restaurant_id = restaurant_id).order_by("-id"), 33 | many=True, 34 | context = {"request":request} 35 | ).data 36 | return JsonResponse({"meals": meals}) 37 | 38 | @csrf_exempt 39 | def customer_add_order(request): 40 | """ 41 | params: 42 | access_token 43 | restaurant_id 44 | address 45 | order_details(json format),examples: 46 | [{"meal_id": 1, "quantity": 2},{"meal_id": 2, "quantity": 3}] 47 | stripe_token 48 | 49 | return: 50 | {"status":"success"} 51 | """ 52 | if request.method == "POST": 53 | # get token 54 | access_token = AccessToken.objects.get(token = request.POST.get("access_token"),expires__gt = timezone.now()) 55 | 56 | #get Profile 57 | customer = access_token.user.customer 58 | 59 | # get stripe token 60 | stripe_token = request.POST["stripe_token"] 61 | 62 | # check whether customer has any order that is not DELIVERED 63 | if Order.objects.filter(customer = customer).exclude(status = Order.DELIVERED): 64 | return JsonResponse({"status":"failed", "error":"Your last order must be completed"}) 65 | 66 | # check address 67 | if not request.POST["address"]: 68 | return JsonResponse({"status":"failed", "error":"Address is required"}) 69 | 70 | # get order details 71 | order_details = json.loads(request.POST["order_details"]) 72 | 73 | order_total = 0 74 | for meal in order_details: 75 | order_total += Meal.objects.get(id=meal["meal_id"]).price * meal["quantity"] 76 | 77 | if len(order_details)>0: 78 | # step 1: create a charge: this will charge customer's card 79 | charge = stripe.Charge.create( 80 | amount = order_total * 100, 81 | currency = "usd", 82 | source = stripe_token, 83 | description = "Foodtasker Order" 84 | ) 85 | 86 | if charge.status != "failed": 87 | # step 2: create an order 88 | order = Order.objects.create( 89 | customer = customer, 90 | restaurant_id = request.POST["restaurant_id"], 91 | total = order_total, 92 | status = Order.COOKING, 93 | address = request.POST["address"] 94 | ) 95 | 96 | # step 3: create order details 97 | for meal in order_details: 98 | OrderDetails.objects.create( 99 | order = order, 100 | meal_id = meal["meal_id"], 101 | quantity = meal["quantity"], 102 | sub_total = Meal.objects.get(id=meal["meal_id"]).price * meal["quantity"] 103 | ) 104 | 105 | return JsonResponse({"status":"success"}) 106 | 107 | else: 108 | return JsonResponse({"status":"failed","error":"Faile connect to Stripe"}) 109 | 110 | def customer_get_latest_order(request): 111 | access_token = AccessToken.objects.get(token=request.GET.get("access_token"),expires__gt=timezone.now()) 112 | 113 | customer = access_token.user.customer 114 | order = OrderSerializer(Order.objects.filter(customer = customer).last()).data 115 | return JsonResponse({"order": order}) 116 | 117 | # GET params: access_token 118 | def customer_driver_location(request): 119 | access_token = AccessToken.objects.get(token=request.GET.get("access_token"),expires__gt=timezone.now()) 120 | 121 | customer = access_token.user.customer 122 | 123 | # get driver's location related to this customer's order 124 | current_order = Order.objects.filter(customer = customer, status = Order.ONTHEWAY).last() 125 | location = current_order.driver.location 126 | 127 | return JsonResponse({"location": location}) 128 | 129 | ############### 130 | # RESTAURANTS # 131 | ############### 132 | 133 | def restaurant_order_notification(request, last_request_time): 134 | """ 135 | select count(*) from Orders 136 | where restaurant = request.user.restaurant AND created_at > last_request_time 137 | """ 138 | notification = Order.objects.filter(restaurant = request.user.restaurant, 139 | created_at__gt = last_request_time).count() 140 | 141 | return JsonResponse({"notification":notification}) 142 | 143 | ########### 144 | # DRIVERS # 145 | ########### 146 | 147 | def driver_get_ready_orders(request): 148 | orders = OrderSerializer( 149 | Order.objects.filter(status=Order.READY, driver=None).order_by("-id"), 150 | many=True 151 | ).data 152 | 153 | return JsonResponse({"orders":orders}) 154 | 155 | @csrf_exempt 156 | # POST params: access_token, order_id 157 | def driver_pick_order(request): 158 | if request.method == "POST": 159 | # get token 160 | access_token = AccessToken.objects.get(token=request.POST.get("access_token"),expires__gt=timezone.now()) 161 | # get driver 162 | driver = access_token.user.driver 163 | # check whether driver can only pick one order at the same time 164 | if Order.objects.filter(driver=driver).exclude(status=Order.ONTHEWAY): 165 | return JsonResponse({"status":"failed", "error": "You can only pick one order at the same time."}) 166 | 167 | try: 168 | order = Order.objects.get( 169 | id = request.POST["order_id"], 170 | driver = None, 171 | status = Order.READY 172 | ) 173 | order.driver = driver 174 | order.status = Order.ONTHEWAY 175 | order.picked_at = timezone.now() 176 | order.save() 177 | 178 | return JsonResponse({"status":"success"}) 179 | 180 | except Order.DoesNotExist: 181 | return JsonResponse({"status":"failed", "error":"This order has been picked up by another."}) 182 | return JsonResponse({}) 183 | 184 | # GET params: access_token 185 | def driver_get_latest_order(request): 186 | access_token = AccessToken.objects.get(token=request.GET.get("access_token"),expires__gt=timezone.now()) 187 | 188 | driver = access_token.user.driver 189 | 190 | order = OrderSerializer( 191 | Order.objects.filter(driver = driver).order_by("picked_at").last() 192 | ).data 193 | 194 | return JsonResponse({"order": order}) 195 | 196 | # POST params: access_token, order_id 197 | @csrf_exempt 198 | def driver_complete_order(request): 199 | access_token = AccessToken.objects.get(token=request.POST.get("access_token"),expires__gt=timezone.now()) 200 | 201 | driver = access_token.user.driver 202 | 203 | order = Order.objects.get( 204 | id = request.POST["order_id"], 205 | driver = driver 206 | ) 207 | order.status = Order.DELIVERED 208 | order.save() 209 | 210 | return JsonResponse({"status": "success"}) 211 | 212 | # GET params: access_token 213 | def driver_get_revenue(request): 214 | access_token = AccessToken.objects.get(token=request.GET.get("access_token"),expires__gt=timezone.now()) 215 | 216 | driver = access_token.user.driver 217 | 218 | from datetime import timedelta 219 | 220 | revenue = {} 221 | today = timezone.now() 222 | current_weekdays = [today + timedelta(days=i) for i in range(0-today.weekday(), 7-today.weekday())] 223 | 224 | for day in current_weekdays: 225 | orders = Order.objects.filter( 226 | driver = driver, 227 | status = Order.DELIVERED, 228 | created_at__year = day.year, 229 | created_at__month = day.month, 230 | created_at__day = day.day, 231 | ) 232 | 233 | revenue[day.strftime("%a")] = sum(order.total for order in orders) 234 | 235 | return JsonResponse({"revenue": revenue}) 236 | 237 | # POST params: access_token, 'lat', 'lng' 238 | @csrf_exempt 239 | def driver_update_location(request): 240 | if request.method == "POST": 241 | access_token = AccessToken.objects.get(token=request.POST.get("access_token"),expires__gt=timezone.now()) 242 | 243 | driver = access_token.user.driver 244 | 245 | # set location string -> database 246 | driver.location = request.POST["location"] 247 | driver.save() 248 | 249 | return JsonResponse({"status":"success"}) 250 | -------------------------------------------------------------------------------- /foodtaskerapp/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class FoodtaskerappConfig(AppConfig): 5 | name = 'foodtaskerapp' 6 | -------------------------------------------------------------------------------- /foodtaskerapp/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from django.contrib.auth.models import User 4 | from foodtaskerapp.models import Restaurant, Meal 5 | 6 | class UserForm(forms.ModelForm): 7 | email = forms.CharField(max_length=100, required=True) 8 | password = forms.CharField(widget=forms.PasswordInput()) 9 | 10 | class Meta: 11 | model = User 12 | fields = ("username", "password", "first_name", "last_name", "email") 13 | 14 | class UserFormForEdit(forms.ModelForm): 15 | email = forms.CharField(max_length=100, required=True) 16 | 17 | class Meta: 18 | model = User 19 | fields = ("first_name", "last_name", "email") 20 | 21 | class RestaurantForm(forms.ModelForm): 22 | class Meta: 23 | model = Restaurant 24 | fields = ("name", "phone", "address", "logo") 25 | 26 | class MealForm(forms.ModelForm): 27 | class Meta: 28 | model = Meal 29 | exclude = ("restaurant",) 30 | -------------------------------------------------------------------------------- /foodtaskerapp/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2018-06-12 13:51 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 | initial = True 13 | 14 | dependencies = [ 15 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='Restaurant', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('name', models.CharField(max_length=500)), 24 | ('phone', models.CharField(max_length=500)), 25 | ('address', models.CharField(max_length=500)), 26 | ('logo', models.ImageField(upload_to='restaurant_logo')), 27 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='restaurant', to=settings.AUTH_USER_MODEL)), 28 | ], 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /foodtaskerapp/migrations/0002_customer_driver.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2018-06-14 08:01 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 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ('foodtaskerapp', '0001_initial'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Customer', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('avatar', models.CharField(max_length=500)), 23 | ('phone', models.CharField(blank=True, max_length=500)), 24 | ('address', models.CharField(blank=True, max_length=500)), 25 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='customer', to=settings.AUTH_USER_MODEL)), 26 | ], 27 | ), 28 | migrations.CreateModel( 29 | name='Driver', 30 | fields=[ 31 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 32 | ('avatar', models.CharField(max_length=500)), 33 | ('phone', models.CharField(blank=True, max_length=500)), 34 | ('address', models.CharField(blank=True, max_length=500)), 35 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='driver', to=settings.AUTH_USER_MODEL)), 36 | ], 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /foodtaskerapp/migrations/0003_meal.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2018-06-16 02:09 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('foodtaskerapp', '0002_customer_driver'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Meal', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('name', models.CharField(max_length=500)), 21 | ('short_description', models.CharField(max_length=500)), 22 | ('image', models.ImageField(upload_to='meal_images/')), 23 | ('price', models.IntegerField(default=0)), 24 | ('restaurant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='foodtaskerapp.Restaurant')), 25 | ], 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /foodtaskerapp/migrations/0004_order.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2018-06-16 05:11 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 | ('foodtaskerapp', '0003_meal'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Order', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('address', models.CharField(max_length=500)), 22 | ('total', models.IntegerField()), 23 | ('status', models.IntegerField(choices=[(1, 'Cooking'), (2, 'Ready'), (3, 'On the way'), (4, 'Delivered')])), 24 | ('created_at', models.DateTimeField(default=django.utils.timezone.now)), 25 | ('picked_at', models.DateTimeField(blank=True, null=True)), 26 | ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='foodtaskerapp.Customer')), 27 | ('driver', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='foodtaskerapp.Driver')), 28 | ('restaurant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='foodtaskerapp.Restaurant')), 29 | ], 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /foodtaskerapp/migrations/0005_orderdetails.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2018-06-16 05:14 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('foodtaskerapp', '0004_order'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='OrderDetails', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('quantity', models.IntegerField()), 21 | ('sub_total', models.IntegerField()), 22 | ('meal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='foodtaskerapp.Meal')), 23 | ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='order_details', to='foodtaskerapp.Order')), 24 | ], 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /foodtaskerapp/migrations/0006_auto_20180616_0927.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2018-06-16 09:27 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('foodtaskerapp', '0005_orderdetails'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='order', 18 | name='driver', 19 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='foodtaskerapp.Driver'), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /foodtaskerapp/migrations/0007_driver_location.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2018-06-18 03:22 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 | ('foodtaskerapp', '0006_auto_20180616_0927'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='driver', 17 | name='location', 18 | field=models.CharField(blank=True, max_length=500), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /foodtaskerapp/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattchw/Foodtasker-django/184c4a776236dfe189b5edc922616faacc2c3244/foodtaskerapp/migrations/__init__.py -------------------------------------------------------------------------------- /foodtaskerapp/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import User 3 | from django.utils import timezone 4 | 5 | # Create your models here. 6 | class Restaurant(models.Model): 7 | user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='restaurant') 8 | name = models.CharField(max_length=500) 9 | phone = models.CharField(max_length=500) 10 | address = models.CharField(max_length=500) 11 | logo = models.ImageField(upload_to='restaurant_logo', blank=False) 12 | 13 | def __str__(self): 14 | return self.name 15 | 16 | class Customer(models.Model): 17 | user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='customer') 18 | avatar = models.CharField(max_length=500) 19 | phone = models.CharField(max_length=500, blank=True) 20 | address = models.CharField(max_length=500, blank=True) 21 | 22 | def __str__(self): 23 | return self.user.get_full_name() 24 | 25 | class Driver(models.Model): 26 | user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='driver') 27 | avatar = models.CharField(max_length=500) 28 | phone = models.CharField(max_length=500, blank=True) 29 | address = models.CharField(max_length=500, blank=True) 30 | location = models.CharField(max_length=500, blank=True) 31 | 32 | def __str__(self): 33 | return self.user.get_full_name() 34 | 35 | class Meal(models.Model): 36 | restaurant = models.ForeignKey(Restaurant) 37 | name = models.CharField(max_length=500) 38 | short_description = models.CharField(max_length=500) 39 | image = models.ImageField(upload_to='meal_images/',blank=False) 40 | price = models.IntegerField(default=0) 41 | 42 | def __str__(self): 43 | return self.name 44 | 45 | class Order(models.Model): 46 | COOKING = 1 47 | READY = 2 48 | ONTHEWAY = 3 49 | DELIVERED = 4 50 | 51 | STATUS_CHOICES =( 52 | (COOKING,"Cooking"), 53 | (READY,"Ready"), 54 | (ONTHEWAY,"On the way"), 55 | (DELIVERED,"Delivered"), 56 | ) 57 | 58 | customer = models.ForeignKey(Customer) 59 | restaurant = models.ForeignKey(Restaurant) 60 | driver = models.ForeignKey(Driver, blank=True, null=True) 61 | address = models.CharField(max_length=500) 62 | total = models.IntegerField() 63 | status = models.IntegerField(choices = STATUS_CHOICES) 64 | created_at = models.DateTimeField(default = timezone.now) 65 | picked_at = models.DateTimeField(blank=True, null=True) 66 | 67 | def __str__(self): 68 | return str(self.id) 69 | 70 | class OrderDetails(models.Model): 71 | order = models.ForeignKey(Order, related_name='order_details') 72 | meal = models.ForeignKey(Meal) 73 | quantity = models.IntegerField() 74 | sub_total = models.IntegerField() 75 | 76 | def __str__(self): 77 | return str(self.id) 78 | -------------------------------------------------------------------------------- /foodtaskerapp/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from foodtaskerapp.models import Restaurant, Meal, Customer, Driver, Order, OrderDetails 4 | 5 | class RestaurantSerializer(serializers.ModelSerializer): 6 | logo = serializers.SerializerMethodField() 7 | 8 | def get_logo(self, restaurant): 9 | request = self.context.get('request') 10 | logo_url = restaurant.logo.url 11 | return request.build_absolute_uri(logo_url) 12 | 13 | class Meta: 14 | model = Restaurant 15 | fields = ("id","name","phone","address","logo") 16 | 17 | class MealSerializer(serializers.ModelSerializer): 18 | image = serializers.SerializerMethodField() 19 | 20 | def get_image(self, meal): 21 | request = self.context.get('request') 22 | image_url = meal.image.url 23 | return request.build_absolute_uri(image_url) 24 | 25 | class Meta: 26 | model = Meal 27 | fields = ("id","name","short_description","image","price") 28 | 29 | # order serializer 30 | class OrderCustomerSerializer(serializers.ModelSerializer): 31 | name = serializers.ReadOnlyField(source="user.get_full_name") 32 | 33 | class Meta: 34 | model = Customer 35 | fields = ("id", "name", "avatar", "phone", "address") 36 | 37 | class OrderDriverSerializer(serializers.ModelSerializer): 38 | name = serializers.ReadOnlyField(source="user.get_full_name") 39 | 40 | class Meta: 41 | model = Customer 42 | fields = ("id", "name", "avatar", "phone", "address") 43 | 44 | class OrderRestaurantSerializer(serializers.ModelSerializer): 45 | class Meta: 46 | model = Restaurant 47 | fields = ("id", "name", "phone", "address") 48 | 49 | class OrderMealSerializer(serializers.ModelSerializer): 50 | class Meta: 51 | model = Meal 52 | fields = ("id", "name", "price") 53 | 54 | class OrderDetailsSerializer(serializers.ModelSerializer): 55 | meal = OrderMealSerializer 56 | 57 | class Meta: 58 | model = OrderDetails 59 | fields = ("id", "meal", "quantity", "sub_total") 60 | 61 | class OrderSerializer(serializers.ModelSerializer): 62 | customer = OrderCustomerSerializer() 63 | driver = OrderDriverSerializer() 64 | restaurant = OrderRestaurantSerializer() 65 | order_details = OrderDetailsSerializer(many=True) 66 | status = serializers.ReadOnlyField(source="get_status_display") 67 | 68 | class Meta: 69 | model = Order 70 | fields = ("id", "customer", "restaurant", "driver", "order_details", "total", "status", "address") 71 | -------------------------------------------------------------------------------- /foodtaskerapp/social_auth_pipeline.py: -------------------------------------------------------------------------------- 1 | from foodtaskerapp.models import Customer, Driver 2 | 3 | def create_user_by_type(backend, user, request, response, *args, **kwargs): 4 | if backend.name == 'facebook': 5 | avatar = 'http://graph.facebook.com/%s/picture?type=large' % response['id'] 6 | 7 | if request['user_type'] == "driver" and not Driver.objects.filter(user_id=user.id): 8 | Driver.objects.create(user_id=user.id, avatar = avatar) 9 | elif not Customer.objects.filter(user_id=user.id): 10 | Customer.objects.create(user_id=user.id, avatar = avatar) 11 | -------------------------------------------------------------------------------- /foodtaskerapp/static/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattchw/Foodtasker-django/184c4a776236dfe189b5edc922616faacc2c3244/foodtaskerapp/static/.DS_Store -------------------------------------------------------------------------------- /foodtaskerapp/static/css/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattchw/Foodtasker-django/184c4a776236dfe189b5edc922616faacc2c3244/foodtaskerapp/static/css/.DS_Store -------------------------------------------------------------------------------- /foodtaskerapp/static/css/style.css: -------------------------------------------------------------------------------- 1 | body{ 2 | padding-top: 50px; 3 | background-color: #EDF2F5; 4 | overflow: auto; 5 | } 6 | .bg-blue{ 7 | background-color: #3F3F63; 8 | } 9 | .bg-gray{ 10 | background-color: #88a1b2; 11 | } 12 | 13 | .text-white{ 14 | color: white; 15 | } 16 | 17 | .btn-pink{ 18 | color: white; 19 | background-color: #EE5462; 20 | } 21 | 22 | .btn-pink:hover, 23 | .btn-pink:focus{ 24 | color: white; 25 | background-color: #DA4F5D; 26 | } 27 | 28 | /*side bar*/ 29 | .sidebar{ 30 | height: 100vh; 31 | position: fixed; 32 | top: 0; 33 | left: 0; 34 | padding: 0; 35 | background-color: #3F3F63; 36 | overflow: auto; 37 | } 38 | 39 | /*side bar - header*/ 40 | .sidebar > div:first-child{ 41 | padding: 30px 10px; 42 | font-weight: bold; 43 | font-size: 15px; 44 | background-color: #383957; 45 | } 46 | 47 | .sidebar > div:first-child > h4{ 48 | color: #E0DEEB; 49 | } 50 | 51 | .sidebar > div:first-child > h5{ 52 | color: #B6B4C1; 53 | } 54 | 55 | /*side bar - list*/ 56 | .sidebar .list-group{ 57 | padding: 0; 58 | background-color: #3F3F63; 59 | } 60 | .sidebar .list-group > span{ 61 | color: #B6B4C1; 62 | display: block; 63 | margin-top: 20px; 64 | padding: 20px 30px; 65 | font-size: 16px; 66 | font-weight: 300; 67 | } 68 | .sidebar .list-group .list-group-item, 69 | .sidebar .list-group .list-group-item:focus{ 70 | background-color: #3f3f63; 71 | color: #b6b4c1; 72 | border: 0; 73 | padding-left: 60px; 74 | padding-right: 30px; 75 | } 76 | .sidebar .list-group .list-group-item.active, 77 | .sidebar .list-group .list-group-item:hover{ 78 | background-color: #383957; 79 | color: #e0deeb; 80 | } 81 | .sidebar .list-group .list-group-item:first-child{ 82 | border-top-left-radius: 0; 83 | border-top-right-radius: 0; 84 | } 85 | .sidebar .list-group .list-group-item .badge, 86 | .sidebar .list-group .list-group-item.active .badge{ 87 | background-color: #ee5462; 88 | color: #e0deeb; 89 | } 90 | /* side bar - logout */ 91 | .sidebar > div:last-child{ 92 | padding: 30px; 93 | } 94 | .sidebar > div:last-child .btn{ 95 | width: 100%; 96 | } 97 | /* content */ 98 | .content{ 99 | padding: 0 40px; 100 | } 101 | .content .panel{ 102 | border: none; 103 | } 104 | 105 | .table > thead > tr > th, 106 | .table > thead > tr > td{ 107 | vertical-align: middle; 108 | } 109 | .table, 110 | .table> thead >tr> th{ 111 | border-color: #88a1b2; 112 | } 113 | -------------------------------------------------------------------------------- /foodtaskerapp/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattchw/Foodtasker-django/184c4a776236dfe189b5edc922616faacc2c3244/foodtaskerapp/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /foodtaskerapp/static/fonts/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | -------------------------------------------------------------------------------- /foodtaskerapp/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattchw/Foodtasker-django/184c4a776236dfe189b5edc922616faacc2c3244/foodtaskerapp/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /foodtaskerapp/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattchw/Foodtasker-django/184c4a776236dfe189b5edc922616faacc2c3244/foodtaskerapp/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /foodtaskerapp/static/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattchw/Foodtasker-django/184c4a776236dfe189b5edc922616faacc2c3244/foodtaskerapp/static/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /foodtaskerapp/static/image/McDonalds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattchw/Foodtasker-django/184c4a776236dfe189b5edc922616faacc2c3244/foodtaskerapp/static/image/McDonalds.png -------------------------------------------------------------------------------- /foodtaskerapp/static/image/burger.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattchw/Foodtasker-django/184c4a776236dfe189b5edc922616faacc2c3244/foodtaskerapp/static/image/burger.jpeg -------------------------------------------------------------------------------- /foodtaskerapp/static/image/restaurant.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattchw/Foodtasker-django/184c4a776236dfe189b5edc922616faacc2c3244/foodtaskerapp/static/image/restaurant.jpeg -------------------------------------------------------------------------------- /foodtaskerapp/static/image/tamjaisamgor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattchw/Foodtasker-django/184c4a776236dfe189b5edc922616faacc2c3244/foodtaskerapp/static/image/tamjaisamgor.jpg -------------------------------------------------------------------------------- /foodtaskerapp/static/js/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattchw/Foodtasker-django/184c4a776236dfe189b5edc922616faacc2c3244/foodtaskerapp/static/js/.DS_Store -------------------------------------------------------------------------------- /foodtaskerapp/static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.7 (http://getbootstrap.com) 3 | * Copyright 2011-2016 Twitter, Inc. 4 | * Licensed under the MIT license 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1||b[0]>3)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){if(a(b.target).is(this))return b.handleObj.handler.apply(this,arguments)}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.7",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a("#"===f?[]:f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.7",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c).prop(c,!0)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c).prop(c,!1))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")?(c.prop("checked")&&(a=!1),b.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==c.prop("type")&&(c.prop("checked")!==this.$element.hasClass("active")&&(a=!1),this.$element.toggleClass("active")),c.prop("checked",this.$element.hasClass("active")),a&&c.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target).closest(".btn");b.call(d,"toggle"),a(c.target).is('input[type="radio"], input[type="checkbox"]')||(c.preventDefault(),d.is("input,button")?d.trigger("focus"):d.find("input:visible,button:visible").first().trigger("focus"))}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.7",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));if(!(a>this.$items.length-1||a<0))return this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){if(!this.sliding)return this.slide("next")},c.prototype.prev=function(){if(!this.sliding)return this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.7",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function c(c){c&&3===c.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=b(d),f={relatedTarget:this};e.hasClass("open")&&(c&&"click"==c.type&&/input|textarea/i.test(c.target.tagName)&&a.contains(e[0],c.target)||(e.trigger(c=a.Event("hide.bs.dropdown",f)),c.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger(a.Event("hidden.bs.dropdown",f)))))}))}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.7",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=b(e),g=f.hasClass("open");if(c(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(a(this)).on("click",c);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger(a.Event("shown.bs.dropdown",h))}return!1}},g.prototype.keydown=function(c){if(/(38|40|27|32)/.test(c.which)&&!/input|textarea/i.test(c.target.tagName)){var d=a(this);if(c.preventDefault(),c.stopPropagation(),!d.is(".disabled, :disabled")){var e=b(d),g=e.hasClass("open");if(!g&&27!=c.which||g&&27==c.which)return 27==c.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find(".dropdown-menu"+h);if(i.length){var j=i.index(c.target);38==c.which&&j>0&&j--,40==c.which&&jdocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);if(c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),!c.isInStateTrue())return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-mo.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null,a.$element=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;!e&&/destroy|hide/.test(b)||(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.7",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.7",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.7",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return e=a-d&&"bottom"},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); -------------------------------------------------------------------------------- /foodtaskerapp/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% block title %}{% endblock %} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% block script %} 17 | 18 | {% endblock %} 19 | 20 | 21 |
22 |
23 | 26 |
27 | {% block page %}{% endblock %} 28 |
29 |
30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /foodtaskerapp/templates/base_signup.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% block title %}{% endblock %} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |

{% block heading %}{% endblock %}

27 |
28 | {% block content %}{% endblock %} 29 |
30 | 31 |
32 | 33 |
34 |
35 | 36 |
37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /foodtaskerapp/templates/restaurant/account.html: -------------------------------------------------------------------------------- 1 | {% extends 'restaurant/base.html' %} 2 | {% load bootstrap3 %} 3 | 4 | {% block page %} 5 |
6 |
7 |
8 |

9 | Account 10 |

11 |
12 |
13 |
14 | {% csrf_token %} 15 | {% bootstrap_form user_form %} 16 | {% bootstrap_form restaurant_form %} 17 | 18 |
19 |
20 |
21 |
22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /foodtaskerapp/templates/restaurant/add_meal.html: -------------------------------------------------------------------------------- 1 | {% extends 'restaurant/base.html' %} 2 | {% load bootstrap3 %} 3 | 4 | {% block page %} 5 |
6 |
7 |
8 |

9 | Add Meal 10 |

11 |
12 |
13 |
14 | {% csrf_token %} 15 | {% bootstrap_form form %} 16 | 17 |
18 |
19 |
20 |
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /foodtaskerapp/templates/restaurant/base.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} Restaurant {% endblock %} 4 | 5 | {% block script %} 6 | 24 | {% endblock %} 25 | 26 | {% block sidebar %} 27 | {% url 'restaurant-order' as restaurant_order_url %} 28 | {% url 'restaurant-meal' as restaurant_meal_url %} 29 | {% url 'restaurant-report' as restaurant_report_url %} 30 | {% url 'restaurant-account' as restaurant_account_url %} 31 |
32 | 33 |
34 |
35 |

hi, {{ request.user.get_full_name }}

36 |
{{ request.user.restaurant.name }}
37 |
38 | 39 |
40 | dashboard 41 | 42 | Orders 43 | 44 | 45 | Meals 46 | Reports 47 | 48 | Profile 49 | 50 | 51 |
52 | 53 |
54 | Logout 55 |
56 | 57 | {% endblock %} 58 | -------------------------------------------------------------------------------- /foodtaskerapp/templates/restaurant/edit_meal.html: -------------------------------------------------------------------------------- 1 | {% extends 'restaurant/base.html' %} 2 | {% load bootstrap3 %} 3 | 4 | {% block page %} 5 |
6 |
7 |
8 |

9 | Edit Meal 10 |

11 |
12 |
13 |
14 | {% csrf_token %} 15 | {% bootstrap_form form %} 16 | 17 |
18 |
19 |
20 |
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /foodtaskerapp/templates/restaurant/meal.html: -------------------------------------------------------------------------------- 1 | {% extends 'restaurant/base.html' %} 2 | 3 | {% block page %} 4 | 5 |
6 |
7 |

8 | Meals 9 |

10 |
11 |
12 |
13 | Add meal 14 |

15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {% for meal in meals %} 28 | 29 | 30 | 33 | 34 | 35 | 38 | 39 | {% endfor %} 40 | 41 |
IDNameShort DescriptionPriceImage
{{meal.id}} 31 | {{meal.name}} 32 | {{meal.short_description}}{{meal.price}} 36 | 37 |
42 |
43 |
44 | 45 | {% endblock %} 46 | -------------------------------------------------------------------------------- /foodtaskerapp/templates/restaurant/order.html: -------------------------------------------------------------------------------- 1 | {% extends 'restaurant/base.html' %} 2 | 3 | {% block page %} 4 |
5 |
6 |

7 | Orders 8 |

9 |
10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% for order in orders %} 26 | 27 | 28 | 33 | 34 | 35 | 36 | 37 | 46 | 47 | {% endfor %} 48 | 49 |
IDOrder DetailsCustomerDriverTotalStatusAction
{{order.id}} 29 | {% for od in order.order_details.all %} 30 | {{od.meal.name}} {{od.meal.price}} x {{od.quantity}} = ${{od.sub_total}} 31 | {% endfor %} 32 | {{order.customer}}{{order.driver}}{{order.total}}{{order.get_status_display}} 38 | {% if order.status == 1 %} 39 |
40 | {% csrf_token %} 41 | 42 | 43 |
44 | {% endif %} 45 |
50 |
51 |
52 | {% endblock %} 53 | -------------------------------------------------------------------------------- /foodtaskerapp/templates/restaurant/report.html: -------------------------------------------------------------------------------- 1 | {% extends 'restaurant/base.html' %} 2 | 3 | {% block script %} 4 | 5 | {% endblock %} 6 | 7 | {% block page %} 8 |
9 |
10 | 11 |
12 |
13 |

14 | Revenue by week 15 |

16 |
17 |
18 | 19 | 37 |
38 |
39 | 40 |
41 |
42 |

43 | Orders by week 44 |

45 |
46 |
47 | 48 | 66 |
67 |
68 | 69 |
70 |
71 | 72 |
73 |
74 |

75 | top 3 meals 76 |

77 |
78 |
79 | 80 | 99 |
100 |
101 | 102 |
103 |
104 |

105 | top 3 drivers 106 |

107 |
108 |
109 | 110 | 129 |
130 |
131 |
132 |
133 | {% endblock %} 134 | -------------------------------------------------------------------------------- /foodtaskerapp/templates/restaurant/sign_in.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_signup.html' %} 2 | {% load bootstrap3 %} 3 | 4 | {% block title %}Sign in{% endblock %} 5 | {% block heading %}Restaurant - Sign in{% endblock %} 6 | 7 | {% block content %} 8 | 9 |
10 | {% csrf_token %} 11 | {% bootstrap_form form %} 12 | 13 |
14 | Become a Restaurant 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /foodtaskerapp/templates/restaurant/sign_up.html: -------------------------------------------------------------------------------- 1 | {% extends 'base_signup.html' %} 2 | {% load bootstrap3 %} 3 | 4 | {% block title %}Sign Up{% endblock %} 5 | {% block heading %}Restaurant - Sign Up{% endblock %} 6 | 7 | {% block content %} 8 | 9 |
10 | {% csrf_token %} 11 | {% bootstrap_form user_form %} 12 | {% bootstrap_form restaurant_form %} 13 | 14 |
15 | Already have an account 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /foodtaskerapp/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /foodtaskerapp/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect 2 | from django.contrib.auth.decorators import login_required 3 | 4 | from foodtaskerapp.forms import UserForm, RestaurantForm, UserFormForEdit, MealForm 5 | from django.contrib.auth import authenticate, login 6 | 7 | from django.contrib.auth.models import User 8 | from foodtaskerapp.models import Meal, Order, Driver 9 | 10 | from django.db.models import Sum, Count, Case, When 11 | 12 | # Create your views here. 13 | def home(request): 14 | return redirect(restaurant_home) 15 | 16 | @login_required(login_url='/restaurant/sign-in/') 17 | def restaurant_home(request): 18 | return redirect(restaurant_order) 19 | 20 | @login_required(login_url='/restaurant/sign-in/') 21 | def restaurant_account(request): 22 | user_form = UserFormForEdit(instance = request.user) 23 | restaurant_form = RestaurantForm(instance = request.user.restaurant) 24 | 25 | if request.method == "POST": 26 | user_form = UserFormForEdit(request.POST, instance = request.user) 27 | restaurant_form = RestaurantForm(request.POST, request.FILES, instance = request.user.restaurant) 28 | 29 | if user_form.is_valid() and restaurant_form.is_valid(): 30 | user_form.save() 31 | restaurant_form.save() 32 | 33 | return render(request, 'restaurant/account.html', { 34 | "user_form": user_form, 35 | "restaurant_form": restaurant_form 36 | }) 37 | 38 | @login_required(login_url='/restaurant/sign-in/') 39 | def restaurant_meal(request): 40 | meals = Meal.objects.filter(restaurant=request.user.restaurant).order_by("-id") 41 | return render(request, 'restaurant/meal.html', {"meals":meals}) 42 | 43 | @login_required(login_url='/restaurant/sign-in/') 44 | def restaurant_add_meal(request): 45 | form = MealForm() 46 | 47 | if request.method == "POST": 48 | form = MealForm(request.POST, request.FILES) 49 | 50 | if form.is_valid(): 51 | meal = form.save(commit=False) 52 | meal.restaurant = request.user.restaurant 53 | meal.save() 54 | return redirect(restaurant_meal) 55 | 56 | return render(request, 'restaurant/add_meal.html', { 57 | "form": form 58 | }) 59 | 60 | @login_required(login_url='/restaurant/sign-in/') 61 | def restaurant_edit_meal(request, meal_id): 62 | form = MealForm(instance = Meal.objects.get(id = meal_id)) 63 | 64 | if request.method == "POST": 65 | form = MealForm(request.POST, request.FILES, instance = Meal.objects.get(id = meal_id)) 66 | 67 | if form.is_valid(): 68 | form.save() 69 | return redirect(restaurant_meal) 70 | 71 | return render(request, 'restaurant/edit_meal.html', { 72 | "form": form 73 | }) 74 | 75 | @login_required(login_url='/restaurant/sign-in/') 76 | def restaurant_order(request): 77 | if request.method == "POST": 78 | order=Order.objects.get(id=request.POST["id"], restaurant = request.user.restaurant) 79 | if order.status == Order.COOKING: 80 | order.status = Order.READY 81 | order.save() 82 | 83 | orders = Order.objects.filter(restaurant = request.user.restaurant).order_by("-id") 84 | return render(request, 'restaurant/order.html', {"orders": orders}) 85 | 86 | @login_required(login_url='/restaurant/sign-in/') 87 | def restaurant_report(request): 88 | # Calculate revenue and number of order by current week 89 | from datetime import datetime, timedelta 90 | 91 | revenue = [] 92 | orders = [] 93 | 94 | # calculate weekdays 95 | today = datetime.now() 96 | current_weekdays = [today + timedelta(days=i) for i in range(0-today.weekday(), 7-today.weekday())] 97 | 98 | for day in current_weekdays: 99 | delivered_orders = Order.objects.filter( 100 | restaurant = request.user.restaurant, 101 | status = Order.DELIVERED, 102 | created_at__year = day.year, 103 | created_at__month = day.month, 104 | created_at__day = day.day, 105 | ) 106 | revenue.append(sum(order.total for order in delivered_orders)) 107 | orders.append(delivered_orders.count()) 108 | 109 | # top 3 Meals 110 | top3_meals = Meal.objects.filter(restaurant=request.user.restaurant)\ 111 | .annotate(total_order=Sum('orderdetails__quantity'))\ 112 | .order_by("-total_order")[:3] 113 | 114 | meal = { 115 | "labels": [meal.name for meal in top3_meals], 116 | "data": [meal.total_order or 0 for meal in top3_meals] 117 | } 118 | 119 | # top 3 drivers 120 | top3_drivers = Driver.objects.annotate( 121 | total_order = Count( 122 | Case( 123 | When(order__restaurant=request.user.restaurant, then=1) 124 | ) 125 | ) 126 | ).order_by("-total_order")[:3] 127 | 128 | driver = { 129 | "labels": [driver.user.get_username() for driver in top3_drivers], 130 | "data": [driver.total_order for driver in top3_drivers] 131 | } 132 | 133 | return render(request, 'restaurant/report.html', { 134 | "revenue": revenue, 135 | "orders": orders, 136 | "meal": meal, 137 | "driver": driver 138 | }) 139 | 140 | def restaurant_sign_up(request): 141 | user_form = UserForm() 142 | restaurant_form = RestaurantForm() 143 | 144 | if request.method == "POST": 145 | user_form = UserForm(request.POST) 146 | restaurant_form = RestaurantForm(request.POST, request.FILES) 147 | #print(user_form.is_valid()) 148 | #print(restaurant_form.is_valid()) 149 | 150 | if user_form.is_valid() and restaurant_form.is_valid(): 151 | new_user = User.objects.create_user(**user_form.cleaned_data) 152 | new_restaurant = restaurant_form.save(commit=False) 153 | new_restaurant.user = new_user 154 | new_restaurant.save() 155 | 156 | login(request,authenticate( 157 | username=user_form.cleaned_data["username"], 158 | password=user_form.cleaned_data["password"] 159 | )) 160 | 161 | return redirect(restaurant_home) 162 | 163 | return render(request, 'restaurant/sign_up.html', { 164 | "user_form": user_form, 165 | "restaurant_form": restaurant_form 166 | }) 167 | -------------------------------------------------------------------------------- /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", "demo.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 | -------------------------------------------------------------------------------- /media/meal_images/burger.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattchw/Foodtasker-django/184c4a776236dfe189b5edc922616faacc2c3244/media/meal_images/burger.jpeg -------------------------------------------------------------------------------- /media/meal_images/burger_4yAvu5x.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattchw/Foodtasker-django/184c4a776236dfe189b5edc922616faacc2c3244/media/meal_images/burger_4yAvu5x.jpeg -------------------------------------------------------------------------------- /media/meal_images/tutorial_-74.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattchw/Foodtasker-django/184c4a776236dfe189b5edc922616faacc2c3244/media/meal_images/tutorial_-74.jpg -------------------------------------------------------------------------------- /media/restaurant_logo/2C.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattchw/Foodtasker-django/184c4a776236dfe189b5edc922616faacc2c3244/media/restaurant_logo/2C.jpg -------------------------------------------------------------------------------- /media/restaurant_logo/2C_o5bAwpp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattchw/Foodtasker-django/184c4a776236dfe189b5edc922616faacc2c3244/media/restaurant_logo/2C_o5bAwpp.jpg -------------------------------------------------------------------------------- /media/restaurant_logo/2C_tmf7CLd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattchw/Foodtasker-django/184c4a776236dfe189b5edc922616faacc2c3244/media/restaurant_logo/2C_tmf7CLd.jpg -------------------------------------------------------------------------------- /media/restaurant_logo/McDonalds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattchw/Foodtasker-django/184c4a776236dfe189b5edc922616faacc2c3244/media/restaurant_logo/McDonalds.png -------------------------------------------------------------------------------- /media/restaurant_logo/_DSC1320v2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattchw/Foodtasker-django/184c4a776236dfe189b5edc922616faacc2c3244/media/restaurant_logo/_DSC1320v2.jpg -------------------------------------------------------------------------------- /media/restaurant_logo/restaurant.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattchw/Foodtasker-django/184c4a776236dfe189b5edc922616faacc2c3244/media/restaurant_logo/restaurant.jpeg -------------------------------------------------------------------------------- /media/restaurant_logo/tamjaisamgor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mattchw/Foodtasker-django/184c4a776236dfe189b5edc922616faacc2c3244/media/restaurant_logo/tamjaisamgor.jpg -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.10 2 | gunicorn==19.6.0 3 | Pillow==3.3.0 4 | whitenoise==3.2.1 5 | dj-database-url==0.4.1 6 | psycopg2==2.7.2 7 | django-rest-framework-social-oauth2==1.0.4 8 | django-bootstrap3==7.0.1 9 | stripe==1.37.0 10 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.6.5 2 | --------------------------------------------------------------------------------