diff --git a/.gitignore b/.gitignore index b6e4761..0604bfd 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ __pycache__/ *.py[cod] *$py.class - +media/ # C extensions *.so @@ -54,6 +54,7 @@ coverage.xml # Translations *.mo *.pot +.idea/ # Django stuff: *.log @@ -127,3 +128,6 @@ dmypy.json # Pyre type checker .pyre/ + +# media +media/ \ No newline at end of file diff --git a/README.md b/README.md index 8b13789..29fd154 100644 --- a/README.md +++ b/README.md @@ -1 +1,48 @@ +Yummy +--- +**This project is going to be a clone of sanpp food with Python and Django framework.** +--- + +## What is yummy? + +It’s actually a service which users can order food very easily and restaurant managers can register their services so +that other people can use them. + +As I said this is going to be a clone of this service , clearly It’s not going to implement all the features + +--- + +## The goal of this project: + +The main purpose of this project is being an acceptable resume and also a good practice of Django framework + +--- + +## Features: + +- [x] We will have two type of users in this project : 1 - Customer 2 - Service Provider. +- [x] Each service provider can provide different services such as (restaurant, fast food, confectionery, supermarket, + …). +- [x] Each service will be able to have a menu containing different items. +- [x] Each service will be able to have custom categories for the items of the menu. +- [x] Each service provider must specify the supported areas for the item delivery for the service. +- [ ] Each service provider can add discount on some of their items for a limited time. +- [x] Each service will have active days and hours. +- [x] Each customer can have different addresses. +- [x] Each customer will be able to see the services(only the supported services in their area). +- [x] Each customer can add items to their cart(note : Each cart can only contain items from one specific service, that + means adding items from different services causes multiple carts). +- [ ] Each customer will be able to add comment for the items which was in their cart after the order was delivered( + note: customers will be able to add one comment for an item after each successful order). +- [ ] Each item will have a score based on its comments. +- [x] Each user will be to see the status of the order after the payment has been successful. +- [ ] The quantity of each item must be increased and decreased at successful orders. + +--- + +## Architecture of the project: + +The project is based on the MVT architecture of the Django framework, so we will use SSR(server side rendering) + +--- diff --git a/accounts/__init__.py b/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/admin.py b/accounts/admin.py new file mode 100644 index 0000000..c87f9ef --- /dev/null +++ b/accounts/admin.py @@ -0,0 +1,22 @@ +from django.contrib import admin +from django.contrib.auth.hashers import make_password + +from accounts.models import Customer, ServiceProvider + + +@admin.register(Customer) +class CustomerAdmin(admin.ModelAdmin): + list_display = ('phone_number', 'first_name', 'last_name', 'date_joined', 'is_active') + list_filter = ('is_active', 'date_joined') + search_fields = ('phone_number',) + + def save_model(self, request, obj, form, change): + obj.password = make_password(form.cleaned_data['password']) + return super().save_model(request, obj, form, change) + + +@admin.register(ServiceProvider) +class ServiceProviderAdmin(admin.ModelAdmin): + list_display = ('username', 'email', 'phone_number', 'date_joined', 'is_active') + list_filter = ('is_active', 'date_joined') + search_fields = ('username', 'phone_number') diff --git a/accounts/apps.py b/accounts/apps.py new file mode 100644 index 0000000..3e3c765 --- /dev/null +++ b/accounts/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'accounts' diff --git a/accounts/authenticate.py b/accounts/authenticate.py new file mode 100644 index 0000000..c350947 --- /dev/null +++ b/accounts/authenticate.py @@ -0,0 +1,42 @@ +from django.contrib.auth.backends import BaseBackend + +from accounts.models import Customer, ServiceProvider + + +class PhoneNumberPasswordBackend(BaseBackend): + def authenticate(self, request, phone_number=None, password=None): + try: + customer = Customer.objects.get(phone_number=phone_number) + except Customer.DoesNotExist: + return None + else: + if password: # if the password is sent via the form + if customer.check_password(password): + return customer + else: + return None + return customer + + def get_user(self, user_id): + try: + customer = Customer.objects.get(pk=user_id) + except Customer.DoesNotExist: + return None + else: + return customer + + +class ServiceProviderAuthentication(BaseBackend): + def authenticate(self, request, username=None, password=None): + try: + user = ServiceProvider.objects.get(username=username, password=password) + return user + + except ServiceProvider.DoesNotExist: + return None + + def get_user(self, user_id): + try: + return ServiceProvider.objects.get(pk=user_id) + except ServiceProvider.DoesNotExist: + return None diff --git a/accounts/forms.py b/accounts/forms.py new file mode 100644 index 0000000..e351ced --- /dev/null +++ b/accounts/forms.py @@ -0,0 +1,166 @@ +from django import forms +from django.contrib.auth import password_validation +from django.core.exceptions import ValidationError +from django.db.models import Q +from django.core.validators import int_list_validator +from django.utils.translation import gettext_lazy as _ + +from accounts.models import ServiceProvider, Customer +from accounts.utils import phone_number_validator + + +class CustomerLoginRegisterForm(forms.Form): + phone_number = forms.CharField(max_length=12, + validators=[ + int_list_validator(message=_('only digits are accepted')), + phone_number_validator + ], + widget=forms.TextInput( + attrs={'class': 'form-control', 'placeholder': 'phone number'}) + ) + + +class CustomerCodeConfirmForm(forms.Form): + code = forms.CharField( + validators=[int_list_validator(message=_('only digits are accepted'))], + widget=forms.TextInput( + attrs={ + 'class': 'form-control', + 'placeholder': _('confirmation code') + } + ) + ) + + +class CustomerPasswordForm(forms.Form): + password = forms.CharField( + widget=forms.PasswordInput( + attrs={ + 'class': 'form-control', + 'placeholder': _('password') + } + ) + ) + + +class CustomerPasswordSetForm(forms.ModelForm): + error_messages = { + 'password_mismatch': _('The two password fields didn’t match.'), + } + password = forms.CharField( + label=_("Password"), + strip=False, + widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}), + help_text=password_validation.password_validators_help_text_html(), + ) + password2 = forms.CharField( + label=_("Password confirmation"), + widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}), + strip=False, + help_text=_("Enter the same password as before, for verification."), + ) + + class Meta: + model = Customer + fields = ('password',) + + def clean_password2(self): + password = self.cleaned_data.get("password") + password2 = self.cleaned_data.get("password2") + if password and password2 and password != password2: + raise ValidationError( + self.error_messages['password_mismatch'], + code='password_mismatch', + ) + return password2 + + def save(self, commit=True): + customer = super().save(commit=False) + customer.set_password(self.cleaned_data["password"]) + if commit: + customer.save() + return customer + + +class CustomerProfileUpdateForm(forms.ModelForm): + class Meta: + model = Customer + fields = ('first_name', 'last_name') + + +class ServiceProviderRegistrationForm(forms.ModelForm): + username = forms.CharField( + max_length=30, + min_length=4, + widget=forms.TextInput( + attrs={ + 'placeholder': 'Username', + 'class': 'form-control'} + ) + ) + phone_number = forms.CharField(max_length=12, + validators=[ + int_list_validator(message=_('only digits are accepted')), + phone_number_validator + ], + widget=forms.TextInput( + attrs={'class': 'form-control', 'placeholder': 'phone number'}) + ) + confirm_password = forms.CharField( + widget=forms.PasswordInput( + attrs={ + 'placeholder': 'Confirm Password', + 'class': 'form-control'} + ) + ) + + class Meta: + model = ServiceProvider + fields = ('username', 'email', 'phone_number', 'password', 'confirm_password') + widgets = { + 'email': forms.EmailInput(attrs={'placeholder': 'Email', 'class': 'form-control'}), + 'phone_number': forms.TextInput(attrs={'placeholder': 'Phone Number', 'class': 'form-control'}), + 'password': forms.PasswordInput(attrs={'placeholder': 'Password', 'class': 'form-control'}), + } + + def clean_confirm_password(self): + if self.cleaned_data['password'] != self.cleaned_data['confirm_password']: + raise ValidationError('passwords not equal!') + return self.cleaned_data['confirm_password'] + + +class ServiceProviderLoginForm(forms.Form): + username = forms.CharField( + min_length=4, + widget=forms.TextInput( + attrs={ + 'placeholder': 'Username, Email, Phone Number', + 'class': 'form-control'} + ) + ) + + password = forms.CharField( + max_length=30, + min_length=4, + widget=forms.PasswordInput( + attrs={ + 'placeholder': 'Password', + 'class': 'form-control'} + ) + ) + + def clean(self): + cleaned_data = super().clean() + + username = cleaned_data['username'] + user = ServiceProvider.objects.filter( + Q(username=username) | + Q(email=username) | + Q(phone_number=username), + ).first() + + if user and user.check_password(cleaned_data['password']): + cleaned_data['user'] = user + return cleaned_data + + raise ValidationError('username or password invalid!') diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..6a7d712 --- /dev/null +++ b/accounts/migrations/0001_initial.py @@ -0,0 +1,62 @@ +# Generated by Django 3.2 on 2021-08-28 20:31 + +import accounts.models +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Customer', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('phone_number', models.CharField(max_length=13, unique=True, verbose_name='phone number')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('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')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ], + options={ + 'verbose_name': 'Customer', + 'verbose_name_plural': 'Customers', + 'db_table': 'customer', + }, + managers=[ + ('objects', accounts.models.CustomerManager()), + ], + ), + migrations.CreateModel( + name='ServiceProvider', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('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.UnicodeUsernameValidator()], verbose_name='username')), + ('email', models.EmailField(blank=True, max_length=254, unique=True, verbose_name='email address')), + ('phone_number', models.CharField(max_length=13, unique=True, verbose_name='phone number')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('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')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ], + options={ + 'verbose_name': 'Service provider', + 'verbose_name_plural': 'Service providers', + 'db_table': 'service_provider', + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/accounts/migrations/0002_auto_20210831_0046.py b/accounts/migrations/0002_auto_20210831_0046.py new file mode 100644 index 0000000..1c7f550 --- /dev/null +++ b/accounts/migrations/0002_auto_20210831_0046.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2 on 2021-08-30 20:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='customer', + name='phone_number', + field=models.CharField(max_length=12, unique=True, verbose_name='phone number'), + ), + migrations.AlterField( + model_name='serviceprovider', + name='phone_number', + field=models.CharField(max_length=12, unique=True, verbose_name='phone number'), + ), + ] diff --git a/accounts/migrations/__init__.py b/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/models.py b/accounts/models.py new file mode 100644 index 0000000..5ef1952 --- /dev/null +++ b/accounts/models.py @@ -0,0 +1,103 @@ +from django.contrib.auth.base_user import AbstractBaseUser +from django.contrib.auth.hashers import make_password +from django.contrib.auth.models import PermissionsMixin, AbstractUser, UserManager +from django.contrib.auth.validators import UnicodeUsernameValidator +from django.db import models +from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ + + +class CustomerManager(UserManager): + def _create_user(self, phone_number, password, **extra_fields): + if not phone_number: + raise ValueError('The given phone_number must be set') + + user = self.model(phone_number=phone_number, **extra_fields) + user.password = make_password(password) + user.save(using=self._db) + return user + + def create_user(self, phone_number, password=None, **extra_fields): + extra_fields.setdefault('is_staff', False) + return self._create_user(phone_number, password, **extra_fields) + + def create(self, phone_number, password=None, **extra_fields): + return self.create_user(phone_number, password, **extra_fields) + + +class Customer(AbstractBaseUser): + phone_number = models.CharField(max_length=12, verbose_name=_('phone number'), unique=True) + first_name = models.CharField(_('first name'), max_length=150, blank=True) + last_name = models.CharField(_('last name'), max_length=150, blank=True) + is_staff = models.BooleanField( + _('staff status'), + default=False, + help_text=_('Designates whether the user can log into this admin site.'), + ) + is_active = models.BooleanField( + _('active'), + default=True, + help_text=_( + 'Designates whether this user should be treated as active. ' + 'Unselect this instead of deleting accounts.' + ), + ) + date_joined = models.DateTimeField(_('date joined'), default=timezone.now) + + USERNAME_FIELD = 'phone_number' + REQUIRED_FIELDS = [] + objects = CustomerManager() + + @property + def full_name(self): + full_name = '%s %s' % (self.first_name, self.last_name) + return full_name.strip() + + def __str__(self): + return f"{self.full_name} - {self.phone_number}" + + class Meta: + verbose_name = _('Customer') + verbose_name_plural = _('Customers') + db_table = 'customer' + + +class ServiceProvider(AbstractBaseUser): + username_validator = UnicodeUsernameValidator() + username = models.CharField( + _('username'), + max_length=150, + unique=True, + help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'), + validators=[username_validator], + error_messages={ + 'unique': _("A user with that username already exists."), + }, + ) + email = models.EmailField(_('email address'), blank=True, unique=True) + phone_number = models.CharField(max_length=12, verbose_name=_('phone number'), unique=True) + is_staff = models.BooleanField( + _('staff status'), + default=False, + help_text=_('Designates whether the user can log into this admin site.'), + ) + is_active = models.BooleanField( + _('active'), + default=True, + help_text=_( + 'Designates whether this user should be treated as active. ' + 'Unselect this instead of deleting accounts.' + ), + ) + date_joined = models.DateTimeField(_('date joined'), default=timezone.now) + + objects = UserManager() + + EMAIL_FIELD = 'email' + USERNAME_FIELD = 'username' + REQUIRED_FIELDS = ['email'] + + class Meta: + verbose_name = _('Service provider') + verbose_name_plural = _('Service providers') + db_table = 'service_provider' diff --git a/accounts/templates/accounts/base.html b/accounts/templates/accounts/base.html new file mode 100644 index 0000000..8a93e4f --- /dev/null +++ b/accounts/templates/accounts/base.html @@ -0,0 +1,27 @@ + + + + + + + + + + {% block title %} {% endblock %} + + +
+ {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + {% block content %} + {% endblock %} +
+ + + \ No newline at end of file diff --git a/accounts/templates/accounts/customer/change_password.html b/accounts/templates/accounts/customer/change_password.html new file mode 100644 index 0000000..5c71871 --- /dev/null +++ b/accounts/templates/accounts/customer/change_password.html @@ -0,0 +1,22 @@ +{% extends 'accounts/base.html' %} +{% block title %} Change Password {% endblock %} +{% block content %} +

Change your password

+
+ {% csrf_token %} + {% for field in form %} + +
+

{{ field }}

+ {% for error in field.errors %} +

{{ error }}

+ {% endfor %} + {% endfor %} +
+ {% if form.errors %} + {% block form_error %} {% endblock %} + {% endif %} + + +
+{% endblock %} \ No newline at end of file diff --git a/accounts/templates/accounts/customer/login_register.html b/accounts/templates/accounts/customer/login_register.html new file mode 100644 index 0000000..d31e4da --- /dev/null +++ b/accounts/templates/accounts/customer/login_register.html @@ -0,0 +1,22 @@ +{% extends 'accounts/base.html' %} +{% block title %} Login - Register {% endblock %} +{% block content %} +

Register - Login

+
+ {% csrf_token %} + {% for field in form %} + +
+

{{ field }}

+ {% for error in field.errors %} +

{{ error }}

+ {% endfor %} + {% endfor %} +
+ {% if form.errors %} + {% block form_error %} {% endblock %} + {% endif %} + + +
+{% endblock %} \ No newline at end of file diff --git a/accounts/templates/accounts/customer/password_confirm.html b/accounts/templates/accounts/customer/password_confirm.html new file mode 100644 index 0000000..b37682c --- /dev/null +++ b/accounts/templates/accounts/customer/password_confirm.html @@ -0,0 +1,22 @@ +{% extends 'accounts/base.html' %} +{% block title %} Password Confirm {% endblock %} +{% block content %} +

Confirm your password

+
+ {% csrf_token %} + {% for field in form %} + +
+

{{ field }}

+ {% for error in field.errors %} +

{{ error }}

+ {% endfor %} + {% endfor %} +
+ {% if form.errors %} + {% block form_error %} {% endblock %} + {% endif %} + + +
+{% endblock %} \ No newline at end of file diff --git a/accounts/templates/accounts/customer/password_set.html b/accounts/templates/accounts/customer/password_set.html new file mode 100644 index 0000000..7c49adb --- /dev/null +++ b/accounts/templates/accounts/customer/password_set.html @@ -0,0 +1,22 @@ +{% extends 'accounts/base.html' %} +{% block title %} Password Set {% endblock %} +{% block content %} +

Set Password

+
+ {% csrf_token %} + {% for field in form %} + +
+

{{ field }}

+ {% for error in field.errors %} +

{{ error }}

+ {% endfor %} + {% endfor %} +
+ {% if form.errors %} + {% block form_error %} {% endblock %} + {% endif %} + + +
+{% endblock %} \ No newline at end of file diff --git a/accounts/templates/accounts/customer/phone_number_confirm.html b/accounts/templates/accounts/customer/phone_number_confirm.html new file mode 100644 index 0000000..675a140 --- /dev/null +++ b/accounts/templates/accounts/customer/phone_number_confirm.html @@ -0,0 +1,22 @@ +{% extends 'accounts/base.html' %} +{% block title %} Phone number confirm {% endblock %} +{% block content %} +

Confirm your phone number

+
+ {% csrf_token %} + {% for field in form %} + +
+

{{ field }}

+ {% for error in field.errors %} +

{{ error }}

+ {% endfor %} + {% endfor %} +
+ {% if form.errors %} + {% block form_error %} {% endblock %} + {% endif %} + + +
+{% endblock %} \ No newline at end of file diff --git a/accounts/templates/accounts/customer/profile.html b/accounts/templates/accounts/customer/profile.html new file mode 100644 index 0000000..5570ffd --- /dev/null +++ b/accounts/templates/accounts/customer/profile.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} +{% block title %} Profile {% endblock %} +{% block content %} +

Profile

+

Phone number : {{ request.user.phone_number }}

+

First name : {{ request.user.first_name }}

+

Last name : {{ request.user.last_name }}

+
+ Address list

+ Orders

+ Create new address

+ + Update profile

+ + {% if not request.user.password %} + Set password +

+ {% else %} + Change password +

+ {% endif %} + +{% endblock %} \ No newline at end of file diff --git a/accounts/templates/accounts/customer/profile_update.html b/accounts/templates/accounts/customer/profile_update.html new file mode 100644 index 0000000..706ccd2 --- /dev/null +++ b/accounts/templates/accounts/customer/profile_update.html @@ -0,0 +1,22 @@ +{% extends 'accounts/base.html' %} +{% block title %} Profile Update {% endblock %} +{% block content %} +

Update your profile

+
+ {% csrf_token %} + {% for field in form %} + +
+

{{ field }}

+ {% for error in field.errors %} +

{{ error }}

+ {% endfor %} + {% endfor %} +
+ {% if form.errors %} + {% block form_error %} {% endblock %} + {% endif %} + + +
+{% endblock %} \ No newline at end of file diff --git a/accounts/templates/accounts/service_provider/change_password.html b/accounts/templates/accounts/service_provider/change_password.html new file mode 100644 index 0000000..2589839 --- /dev/null +++ b/accounts/templates/accounts/service_provider/change_password.html @@ -0,0 +1,9 @@ +{% extends 'accounts/base.html' %} +{% block content %} +

Change your password

+
+ {% csrf_token %} + {{ form.as_p }} + +
+{% endblock %} \ No newline at end of file diff --git a/accounts/templates/accounts/service_provider/login.html b/accounts/templates/accounts/service_provider/login.html new file mode 100644 index 0000000..3a9ada0 --- /dev/null +++ b/accounts/templates/accounts/service_provider/login.html @@ -0,0 +1,8 @@ +{% extends 'accounts/base.html' %} +{% block content %} +
+ {% csrf_token %} + {{ form.as_p }} + +
+{% endblock %} diff --git a/accounts/templates/accounts/service_provider/profile.html b/accounts/templates/accounts/service_provider/profile.html new file mode 100644 index 0000000..69b086f --- /dev/null +++ b/accounts/templates/accounts/service_provider/profile.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% block content %} +

Profile

+

Your username : {{ request.user.username }}

+

Your email : {{ request.user.email }}

+

Your phone number : {{ request.user.phone_number }}

+

+ Service List +

+ Change password +{% endblock %} diff --git a/accounts/templates/accounts/service_provider/registration.html b/accounts/templates/accounts/service_provider/registration.html new file mode 100644 index 0000000..3a9ada0 --- /dev/null +++ b/accounts/templates/accounts/service_provider/registration.html @@ -0,0 +1,8 @@ +{% extends 'accounts/base.html' %} +{% block content %} +
+ {% csrf_token %} + {{ form.as_p }} + +
+{% endblock %} diff --git a/accounts/templatetags/__init__.py b/accounts/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/templatetags/accounts_tags.py b/accounts/templatetags/accounts_tags.py new file mode 100644 index 0000000..ea17538 --- /dev/null +++ b/accounts/templatetags/accounts_tags.py @@ -0,0 +1,29 @@ +from django import template +from django.contrib.auth import get_user_model + +from accounts.models import Customer, ServiceProvider + +User = get_user_model() + +register = template.Library() + + +@register.simple_tag +def is_customer(user): + if isinstance(user, Customer): + return True + return False + + +@register.simple_tag +def is_service_provider(user): + if isinstance(user, ServiceProvider): + return True + return False + + +@register.simple_tag +def is_admin_user(user): + if isinstance(user, User): + return True + return False diff --git a/accounts/tests.py b/accounts/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/accounts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/accounts/urls.py b/accounts/urls.py new file mode 100644 index 0000000..8a65417 --- /dev/null +++ b/accounts/urls.py @@ -0,0 +1,29 @@ +from django.urls import path, reverse_lazy +from django.contrib.auth.views import LogoutView + +from accounts.views import CustomerLoginRegisterView, CustomerPhoneNumberConfirmView, CustomerPasswordConfirmView, \ + CustomerProfileView, CustomerSetPasswordView, ServiceProviderRegistrationView, ServiceProviderLoginView, \ + ServiceProviderProfileView, CustomerProfileUpdateView, CustomerChangePasswordView, \ + ServiceProviderChangePasswordView, CustomerLogoutView + +app_name = 'accounts' + +urlpatterns = [ + path('customer/login-register/', CustomerLoginRegisterView.as_view(), name='customer-login-register'), + path('customer/code-confirm/', CustomerPhoneNumberConfirmView.as_view(), name='customer-code-confirm'), + path('customer/password-confirm/', CustomerPasswordConfirmView.as_view(), name='customer-password-confirm'), + path('customer/profile/', CustomerProfileView.as_view(), name='customer-profile'), + path('customer/profile-update/', CustomerProfileUpdateView.as_view(), name='customer-profile-update'), + path('customer/set-password/', CustomerSetPasswordView.as_view(), name='customer-set-password'), + path('customer/change-password/', CustomerChangePasswordView.as_view(), name='customer-change-password'), + path('customer/logout/', CustomerLogoutView.as_view(), name='customer-logout'), + path( + 'serviceprovider/registration/', ServiceProviderRegistrationView.as_view(), name='service-provider-registration' + ), + path('serviceprovider/login/', ServiceProviderLoginView.as_view(), name='service-provider-login'), + path('serviceprovider/profile/', ServiceProviderProfileView.as_view(), name='service-provider-profile'), + path('serviceprovider/change-password/', ServiceProviderChangePasswordView.as_view(), + name='service-provider-change-password'), + path('serviceprovider/logout/', LogoutView.as_view(next_page=reverse_lazy('accounts:service-provider-login')), + name='service-provider-logout'), +] diff --git a/accounts/utils.py b/accounts/utils.py new file mode 100644 index 0000000..ff757c0 --- /dev/null +++ b/accounts/utils.py @@ -0,0 +1,55 @@ +from datetime import datetime, timedelta +from random import randint + +from django.contrib.auth.mixins import UserPassesTestMixin +from django.core.exceptions import ValidationError + +from accounts.models import Customer, ServiceProvider + + +def phone_number_validator(value): + if not value.startswith('98') or len(value) != 12: + raise ValidationError('phone number must be like 98912*******') + + +def check_expire_time(request): + try: + expire_time = datetime.strptime(request.session['created-time'], '%Y-%m-%d %H:%M:%S') + except KeyError: + expire_time = None + + if expire_time: + now = datetime.strptime(datetime.now().strftime('%Y-%m-%d %H:%M:%S'), '%Y-%m-%d %H:%M:%S') + if (now - expire_time) > timedelta(minutes=2): + del request.session['code'] + del request.session['created-time'] + + +def set_phone_number_session(request, phone_number): + request.session['phone_number'] = phone_number + request.session['code'] = randint(1000, 9999) + request.session['created_time'] = str(datetime.now().strftime('%Y-%m-%d %H:%M:%S')) + print(request.session['code']) + print(request.session['created_time']) + + +def check_is_not_authenticated(user): + return not user.is_authenticated + + +def can_set_password(user): + return not user.password + + +class IsCustomer(UserPassesTestMixin): + raise_exception = True + + def test_func(self): + return isinstance(self.request.user, Customer) + + +class IsServiceProvider(UserPassesTestMixin): + raise_exception = True + + def test_func(self): + return isinstance(self.request.user, ServiceProvider) diff --git a/accounts/views.py b/accounts/views.py new file mode 100644 index 0000000..7657085 --- /dev/null +++ b/accounts/views.py @@ -0,0 +1,214 @@ +from django.contrib import messages +from django.contrib.auth.decorators import login_required, user_passes_test as user_test +from django.contrib.auth.views import PasswordChangeView, LogoutView +from django.http import HttpResponseRedirect +from django.shortcuts import redirect +from django.urls import reverse_lazy +from django.utils.decorators import method_decorator +from django.views.decorators.http import require_http_methods +from django.views.generic import FormView, TemplateView, UpdateView +from django.contrib.auth import authenticate, login, logout +from django.contrib.auth.hashers import make_password + +from accounts.forms import CustomerLoginRegisterForm, CustomerCodeConfirmForm, CustomerPasswordForm, \ + CustomerPasswordSetForm, ServiceProviderRegistrationForm, ServiceProviderLoginForm, CustomerProfileUpdateForm +from accounts.models import Customer +from accounts.utils import check_expire_time, set_phone_number_session, check_is_not_authenticated, \ + IsCustomer, IsServiceProvider +from library.utils import CustomUserPasses + + +@method_decorator(require_http_methods(['GET']), name='dispatch') +@method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch') +class CustomerProfileView(IsCustomer, TemplateView): + template_name = 'accounts/customer/profile.html' + + +@method_decorator(require_http_methods(['GET', 'POST']), name='dispatch') +@method_decorator(user_test(check_is_not_authenticated, login_url=reverse_lazy('accounts:customer-profile')), + name='dispatch') +class CustomerLoginRegisterView(FormView): + form_class = CustomerLoginRegisterForm + template_name = 'accounts/customer/login_register.html' + success_url = reverse_lazy('accounts:customer-code-confirm') + + def form_valid(self, form): + phone_number = form.cleaned_data['phone_number'] + try: + customer = Customer.objects.get(phone_number=phone_number) + except Customer.DoesNotExist: + set_phone_number_session(self.request, phone_number) + else: + if customer.password: + self.success_url = reverse_lazy('accounts:customer-password-confirm') + self.request.session['phone_number'] = phone_number + else: + set_phone_number_session(self.request, phone_number) + + return super().form_valid(form) + + +@method_decorator(require_http_methods(['GET', 'POST']), name='dispatch') +@method_decorator(user_test(check_is_not_authenticated, login_url=reverse_lazy('accounts:customer-profile')), + name='dispatch') +class CustomerPhoneNumberConfirmView(FormView): + form_class = CustomerCodeConfirmForm + template_name = 'accounts/customer/phone_number_confirm.html' + success_url = reverse_lazy('accounts:customer-profile') + + def dispatch(self, request, *args, **kwargs): + check_expire_time(request) + return super().dispatch(request, *args, **kwargs) + + def form_valid(self, form): + phone_number = self.request.session['phone_number'] + form_code = int(form.cleaned_data['code']) + session_code = self.request.session.get('code', None) + + if session_code: + if session_code == form_code: + Customer.objects.get_or_create(phone_number=phone_number) + self.delete_confirm_code() + customer = authenticate(phone_number=phone_number) + if customer: + login(self.request, customer) + messages.info(self.request, 'Login success', 'success') + return super().form_valid(form) + else: + return super().form_valid(form) + + else: + messages.info(self.request, 'The code is incorrect!', 'danger') + return redirect('accounts:customer-code-confirm') + else: + messages.info(self.request, 'The code is invalid! Enter your phone number again', 'danger') + return redirect('accounts:customer-login-register') + + def delete_confirm_code(self): + del self.request.session['code'] + + +@method_decorator(require_http_methods(['GET', 'POST']), name='dispatch') +@method_decorator(user_test(check_is_not_authenticated, login_url=reverse_lazy('accounts:customer-profile')), + name='dispatch') +class CustomerPasswordConfirmView(FormView): + form_class = CustomerPasswordForm + template_name = 'accounts/customer/password_confirm.html' + success_url = reverse_lazy('accounts:customer-profile') + + def form_valid(self, form): + phone_number = self.request.session['phone_number'] + password = form.cleaned_data['password'] + customer = authenticate(phone_number=phone_number, password=password) + + if customer: + login(self.request, customer) + messages.info(self.request, 'Login success', 'success') + return super().form_valid(form) + + else: + messages.info(self.request, 'Your password is incorrect!', 'danger') + return redirect('accounts:customer-password-confirm') + + +@method_decorator(require_http_methods(['GET', 'POST']), name='dispatch') +@method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch') +class CustomerSetPasswordView(CustomUserPasses, UpdateView): + model = Customer + form_class = CustomerPasswordSetForm + success_url = reverse_lazy('accounts:customer-login-register') + template_name = 'accounts/customer/password_set.html' + + def test_func(self): + if not isinstance(self.request.user, Customer): + return False + if self.request.user.password: + return False, True, reverse_lazy("accounts:customer-change-password") + + return True + + def get_object(self, queryset=None): + return self.request.user + + +@method_decorator(require_http_methods(['GET', 'POST']), name='dispatch') +@method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch') +class CustomerProfileUpdateView(IsCustomer, UpdateView): + model = Customer + form_class = CustomerProfileUpdateForm + success_url = reverse_lazy('accounts:customer-profile') + template_name = 'accounts/customer/profile_update.html' + + def get_object(self, queryset=None): + return self.request.user + + +@method_decorator(require_http_methods(['GET', 'POST']), name='dispatch') +@method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch') +class CustomerChangePasswordView(CustomUserPasses, PasswordChangeView): + template_name = 'accounts/customer/change_password.html' + success_url = reverse_lazy('accounts:customer-profile') + + def test_func(self): + if not isinstance(self.request.user, Customer): + return False + if not self.request.user.password: + return False, True, reverse_lazy("accounts:customer-set-password") + return True + + +class CustomerLogoutView(LogoutView): + next_page = reverse_lazy('accounts:customer-login-register') + + def dispatch(self, request, *args, **kwargs): + logout(request) + next_page = self.get_next_page() + response = HttpResponseRedirect(next_page) + if request.COOKIES.get('cart_id', None) is not None: + response.delete_cookie('cart_id') + return response + + +@method_decorator(require_http_methods(['POST', 'GET']), name='dispatch') +@method_decorator(user_test(check_is_not_authenticated, login_url=reverse_lazy('accounts:service-provider-profile')), + name='dispatch') +class ServiceProviderRegistrationView(FormView): + form_class = ServiceProviderRegistrationForm + template_name = 'accounts/service_provider/registration.html' + success_url = reverse_lazy('accounts:service-provider-login') + + def form_valid(self, form): + instance = form.save(commit=False) + instance.password = make_password(instance.password) + instance.save() + return super().form_valid(form) + + +@method_decorator(require_http_methods(['POST', 'GET']), name='dispatch') +@method_decorator(user_test(check_is_not_authenticated, login_url=reverse_lazy('accounts:service-provider-profile')), + name='dispatch') +class ServiceProviderLoginView(FormView): + form_class = ServiceProviderLoginForm + template_name = 'accounts/service_provider/login.html' + success_url = reverse_lazy('accounts:service-provider-profile') + + def form_valid(self, form): + user = form.cleaned_data['user'] + user_authenticated = authenticate(username=user.username, password=user.password) + if user_authenticated: + login(self.request, user_authenticated) + return super().form_valid(form) + + +@method_decorator(require_http_methods(['GET']), name='dispatch') +@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') +class ServiceProviderProfileView(IsServiceProvider, TemplateView): + template_name = 'accounts/service_provider/profile.html' + raise_exception = True + + +@method_decorator(require_http_methods(['GET', 'POST']), name='dispatch') +@method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch') +class ServiceProviderChangePasswordView(IsServiceProvider, PasswordChangeView): + template_name = 'accounts/service_provider/change_password.html' + success_url = reverse_lazy('accounts:service-provider-profile') diff --git a/address/__init__.py b/address/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/address/admin.py b/address/admin.py new file mode 100644 index 0000000..4934784 --- /dev/null +++ b/address/admin.py @@ -0,0 +1,36 @@ +from django.contrib import admin +from .models import State, City, Area, ServiceAddress, CustomerAddress + + +@admin.register(State) +class StateAdmin(admin.ModelAdmin): + list_display = ('name', 'slug') + search_fields = ('name',) + + +@admin.register(City) +class CityAdmin(admin.ModelAdmin): + list_display = ('name', 'slug', 'state') + list_filter = ('state',) + search_fields = ('name',) + + +@admin.register(Area) +class AreaAdmin(admin.ModelAdmin): + list_display = ('name', 'slug', 'city') + list_filter = ('city',) + search_fields = ('name',) + + +@admin.register(ServiceAddress) +class ServiceAddressAdmin(admin.ModelAdmin): + list_display = ('state', 'city', 'area', 'floor', 'plaque') + list_filter = ('state', 'city') + search_fields = ('area', 'floor', 'plaque') + + +@admin.register(CustomerAddress) +class CustomerAddressAdmin(admin.ModelAdmin): + list_display = ('customer_user', 'state', 'city', 'area', 'floor', 'plaque') + list_filter = ('state', 'city') + search_fields = ('area', 'floor', 'plaque') diff --git a/address/apps.py b/address/apps.py new file mode 100644 index 0000000..572d7e0 --- /dev/null +++ b/address/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AddressConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'address' diff --git a/address/forms.py b/address/forms.py new file mode 100644 index 0000000..5d9d061 --- /dev/null +++ b/address/forms.py @@ -0,0 +1,28 @@ +from django import forms + +from address.models import CustomerAddress, ServiceAddress + +base_fields = ('state', 'city', 'area', 'street', 'alley', 'floor', 'plaque') +base_widgets = { + 'state': forms.Select(attrs={'class': 'form-control'}), + 'city': forms.Select(attrs={'class': 'form-control'}), + 'area': forms.Select(attrs={'class': 'form-control'}), + 'street': forms.TextInput(attrs={'class': 'form-control'}), + 'alley': forms.TextInput(attrs={'class': 'form-control'}), + 'floor': forms.NumberInput(attrs={'class': 'form-control'}), + 'plaque': forms.NumberInput(attrs={'class': 'form-control'}), +} + + +class CustomerAddressCreateUpdateForm(forms.ModelForm): + class Meta: + model = CustomerAddress + fields = base_fields + widgets = base_widgets + + +class ServiceAddressCreateUpdateForm(forms.ModelForm): + class Meta: + model = ServiceAddress + fields = base_fields + widgets = base_widgets diff --git a/address/migrations/0001_initial.py b/address/migrations/0001_initial.py new file mode 100644 index 0000000..57ac86c --- /dev/null +++ b/address/migrations/0001_initial.py @@ -0,0 +1,80 @@ +# Generated by Django 3.2 on 2021-08-28 20:31 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='State', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), + ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), + ('name', models.CharField(max_length=20, verbose_name='name')), + ('slug', models.SlugField(allow_unicode=True, max_length=25, verbose_name='slug')), + ], + options={ + 'verbose_name': 'State', + 'verbose_name_plural': 'States', + 'db_table': 'state', + }, + ), + migrations.CreateModel( + name='City', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), + ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), + ('name', models.CharField(max_length=20, verbose_name='name')), + ('slug', models.SlugField(allow_unicode=True, max_length=25, verbose_name='slug')), + ('state', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cities', to='address.state', verbose_name='state')), + ], + options={ + 'verbose_name': 'City', + 'verbose_name_plural': 'Cities', + 'db_table': 'city', + }, + ), + migrations.CreateModel( + name='Area', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), + ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), + ('name', models.CharField(max_length=20, verbose_name='name')), + ('slug', models.SlugField(allow_unicode=True, max_length=25, verbose_name='slug')), + ('city', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='areas', to='address.city', verbose_name='city')), + ], + options={ + 'verbose_name': 'Area', + 'verbose_name_plural': 'Areas', + 'db_table': 'area', + }, + ), + migrations.CreateModel( + name='Address', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), + ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), + ('floor', models.SmallIntegerField(verbose_name='floor')), + ('plaque', models.SmallIntegerField(verbose_name='plaque')), + ('area', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='addresses', to='address.area', verbose_name='area')), + ('city', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='addresses', to='address.city', verbose_name='city')), + ('state', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='addresses', to='address.state', verbose_name='state')), + ], + options={ + 'verbose_name': 'Address', + 'verbose_name_plural': 'Addresses', + 'db_table': 'address', + }, + ), + ] diff --git a/address/migrations/0002_auto_20210830_1305.py b/address/migrations/0002_auto_20210830_1305.py new file mode 100644 index 0000000..7350549 --- /dev/null +++ b/address/migrations/0002_auto_20210830_1305.py @@ -0,0 +1,55 @@ +# Generated by Django 3.2 on 2021-08-30 08:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('address', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='CustomerAddress', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), + ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), + ('floor', models.SmallIntegerField(verbose_name='floor')), + ('plaque', models.SmallIntegerField(verbose_name='plaque')), + ], + options={ + 'verbose_name': 'CustomerAddress', + 'verbose_name_plural': 'CustomerAddresses', + 'db_table': 'customer_address', + }, + ), + migrations.CreateModel( + name='ServiceAddress', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), + ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), + ('floor', models.SmallIntegerField(verbose_name='floor')), + ('plaque', models.SmallIntegerField(verbose_name='plaque')), + ], + options={ + 'verbose_name': 'ServiceAddress', + 'verbose_name_plural': 'ServiceAddresses', + 'db_table': 'service_address', + }, + ), + migrations.RemoveField( + model_name='address', + name='area', + ), + migrations.RemoveField( + model_name='address', + name='city', + ), + migrations.RemoveField( + model_name='address', + name='state', + ), + ] diff --git a/address/migrations/0003_auto_20210830_1305.py b/address/migrations/0003_auto_20210830_1305.py new file mode 100644 index 0000000..b2a0ccc --- /dev/null +++ b/address/migrations/0003_auto_20210830_1305.py @@ -0,0 +1,55 @@ +# Generated by Django 3.2 on 2021-08-30 08:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('service', '0002_alter_service_address'), + ('address', '0002_auto_20210830_1305'), + ('accounts', '0001_initial'), + ('payment', '0002_alter_invoice_address'), + ] + + operations = [ + migrations.DeleteModel( + name='Address', + ), + migrations.AddField( + model_name='serviceaddress', + name='area', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='s_addresses', to='address.area', verbose_name='area'), + ), + migrations.AddField( + model_name='serviceaddress', + name='city', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='s_addresses', to='address.city', verbose_name='city'), + ), + migrations.AddField( + model_name='serviceaddress', + name='state', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='s_addresses', to='address.state', verbose_name='state'), + ), + migrations.AddField( + model_name='customeraddress', + name='area', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='c_addresses', to='address.area', verbose_name='area'), + ), + migrations.AddField( + model_name='customeraddress', + name='city', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='c_addresses', to='address.city', verbose_name='city'), + ), + migrations.AddField( + model_name='customeraddress', + name='state', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='c_addresses', to='address.state', verbose_name='state'), + ), + migrations.AddField( + model_name='customeraddress', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='c_addresses', to='accounts.customer', verbose_name='customer'), + ), + ] diff --git a/address/migrations/0004_auto_20210830_1327.py b/address/migrations/0004_auto_20210830_1327.py new file mode 100644 index 0000000..6577812 --- /dev/null +++ b/address/migrations/0004_auto_20210830_1327.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2 on 2021-08-30 08:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('address', '0003_auto_20210830_1305'), + ] + + operations = [ + migrations.AddField( + model_name='customeraddress', + name='alley', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='customeraddress', + name='street', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='serviceaddress', + name='alley', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='serviceaddress', + name='street', + field=models.TextField(blank=True), + ), + ] diff --git a/address/migrations/0005_auto_20210830_1329.py b/address/migrations/0005_auto_20210830_1329.py new file mode 100644 index 0000000..dc9e112 --- /dev/null +++ b/address/migrations/0005_auto_20210830_1329.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2 on 2021-08-30 08:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('address', '0004_auto_20210830_1327'), + ] + + operations = [ + migrations.AlterField( + model_name='customeraddress', + name='alley', + field=models.CharField(max_length=30), + ), + migrations.AlterField( + model_name='customeraddress', + name='street', + field=models.CharField(max_length=50), + ), + migrations.AlterField( + model_name='serviceaddress', + name='alley', + field=models.CharField(max_length=30), + ), + migrations.AlterField( + model_name='serviceaddress', + name='street', + field=models.CharField(max_length=50), + ), + ] diff --git a/address/migrations/0006_rename_user_customeraddress_customer_user.py b/address/migrations/0006_rename_user_customeraddress_customer_user.py new file mode 100644 index 0000000..3b35923 --- /dev/null +++ b/address/migrations/0006_rename_user_customeraddress_customer_user.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2 on 2021-08-31 10:06 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('address', '0005_auto_20210830_1329'), + ] + + operations = [ + migrations.RenameField( + model_name='customeraddress', + old_name='user', + new_name='customer_user', + ), + ] diff --git a/address/migrations/__init__.py b/address/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/address/models.py b/address/models.py new file mode 100644 index 0000000..bedac10 --- /dev/null +++ b/address/models.py @@ -0,0 +1,90 @@ +from django.db import models + +from accounts.models import Customer +from library.models import BaseModel +from django.utils.translation import ugettext_lazy as _ + + +class State(BaseModel): + name = models.CharField(max_length=20, verbose_name=_('name')) + slug = models.SlugField(max_length=25, verbose_name=_('slug'), allow_unicode=True) + + def __str__(self): + return self.name + + class Meta: + verbose_name = _('State') + verbose_name_plural = _('States') + db_table = 'state' + + +class City(BaseModel): + name = models.CharField(max_length=20, verbose_name=_('name')) + slug = models.SlugField(max_length=25, verbose_name=_('slug'), allow_unicode=True) + state = models.ForeignKey(State, verbose_name=_('state'), related_name='cities', on_delete=models.CASCADE) + + def __str__(self): + return f'{self.state} - {self.name}' + + class Meta: + verbose_name = _('City') + verbose_name_plural = _('Cities') + db_table = 'city' + + +class Area(BaseModel): + name = models.CharField(max_length=20, verbose_name=_('name')) + slug = models.SlugField(max_length=25, verbose_name=_('slug'), allow_unicode=True) + city = models.ForeignKey(City, verbose_name=_('city'), related_name='areas', on_delete=models.CASCADE) + + def __str__(self): + return f'{self.city.name} - {self.name}' + + class Meta: + verbose_name = _('Area') + verbose_name_plural = _('Areas') + db_table = 'area' + + +class BaseAddress(BaseModel): + street = models.CharField(max_length=50) + alley = models.CharField(max_length=30) + floor = models.SmallIntegerField(verbose_name=_('floor')) + plaque = models.SmallIntegerField(verbose_name=_('plaque')) + + class Meta: + abstract = True + + +class CustomerAddress(BaseAddress): + customer_user = models.ForeignKey( + Customer, + verbose_name=_('customer'), + related_name='c_addresses', + on_delete=models.CASCADE + ) + state = models.ForeignKey(State, verbose_name=_('state'), related_name='c_addresses', on_delete=models.CASCADE) + city = models.ForeignKey(City, verbose_name=_('city'), related_name='c_addresses', on_delete=models.CASCADE) + area = models.ForeignKey(Area, verbose_name=_('area'), related_name='c_addresses', on_delete=models.CASCADE) + + def __str__(self): + return f'{self.customer_user} - {self.city} - {self.area}' + + class Meta: + verbose_name = _('CustomerAddress') + verbose_name_plural = _('CustomerAddresses') + db_table = 'customer_address' + + +class ServiceAddress(BaseAddress): + state = models.ForeignKey(State, verbose_name=_('state'), related_name='s_addresses', on_delete=models.CASCADE) + city = models.ForeignKey(City, verbose_name=_('city'), related_name='s_addresses', on_delete=models.CASCADE) + area = models.ForeignKey(Area, verbose_name=_('area'), related_name='s_addresses', on_delete=models.CASCADE) + + def __str__(self): + return f'{self.area.name}، {self.street}، {self.alley}' + + class Meta: + verbose_name = _('ServiceAddress') + verbose_name_plural = _('ServiceAddresses') + db_table = 'service_address' diff --git a/address/templates/address/create_update_form.html b/address/templates/address/create_update_form.html new file mode 100644 index 0000000..a7b816f --- /dev/null +++ b/address/templates/address/create_update_form.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} +{% load accounts_tags %} +{% block content %} +
+ {% csrf_token %} + {{ form.as_p }} + +
+ {% is_service_provider request.user as is_provider %} + {% is_customer request.user as is_customer %} + {% if is_provider %} + {% if service %} + cancel + + {% else %} + cancel + {% endif %} + {% elif is_customer %} + cancel + {% endif %} +{% endblock %} diff --git a/address/templates/address/customer_address_list.html b/address/templates/address/customer_address_list.html new file mode 100644 index 0000000..a3e7908 --- /dev/null +++ b/address/templates/address/customer_address_list.html @@ -0,0 +1,19 @@ +{% extends 'base.html' %} +{% block content %} + Create new address

+ + +{% endblock %} diff --git a/address/templates/address/delete_form.html b/address/templates/address/delete_form.html new file mode 100644 index 0000000..452edf9 --- /dev/null +++ b/address/templates/address/delete_form.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% block content %} +
+
+ {% csrf_token %} +

Are you sure to delete address ({{ object.state }} | {{ object.area }})?

+ +
+ cancel +
+{% endblock %} diff --git a/address/tests.py b/address/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/address/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/address/urls.py b/address/urls.py new file mode 100644 index 0000000..c85a8c8 --- /dev/null +++ b/address/urls.py @@ -0,0 +1,17 @@ +from django.urls import path + +from .views import CustomerAddressCreateView, CustomerAddressUpdateView, CustomerAddressDeleteView, \ + ServiceAddressCreateView, ServiceAddressUpdateView, CustomerAddressListView + +app_name = 'address' + +urlpatterns = [ + path('customer/create/', CustomerAddressCreateView.as_view(), name='customer-address-create'), + path('service/create//', ServiceAddressCreateView.as_view(), name='service-address-create'), + + path('customer/update//', CustomerAddressUpdateView.as_view(), name='customer-address-update'), + path('service/update//', ServiceAddressUpdateView.as_view(), name='service-address-update'), + + path('customer/delete//', CustomerAddressDeleteView.as_view(), name='customer-address-delete'), + path('customer/list/', CustomerAddressListView.as_view(), name='customer-address-list'), +] diff --git a/address/views.py b/address/views.py new file mode 100644 index 0000000..b041e85 --- /dev/null +++ b/address/views.py @@ -0,0 +1,115 @@ +from django.contrib.auth.decorators import login_required +from django.db import transaction +from django.shortcuts import get_object_or_404 +from django.urls import reverse_lazy +from django.utils.decorators import method_decorator +from django.views.generic import CreateView, UpdateView, DeleteView, ListView + +from accounts.models import ServiceProvider +from accounts.utils import IsCustomer, IsServiceProvider +from address.forms import CustomerAddressCreateUpdateForm, ServiceAddressCreateUpdateForm +from address.models import CustomerAddress, ServiceAddress +from library.utils import CustomUserPasses +from service.models import Service + + +class BaseAddress: + model = CustomerAddress + form_class = CustomerAddressCreateUpdateForm + template_name = 'address/create_update_form.html' + + +@method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch') +class CustomerAddressCreateView(BaseAddress, IsCustomer, CreateView): + success_url = reverse_lazy('accounts:customer-profile') + + def form_valid(self, form): + instance = form.save(commit=False) + instance.customer_user = self.request.user + instance.save() + return super().form_valid(form) + + +@method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch') +class CustomerAddressUpdateView(BaseAddress, IsCustomer, UpdateView): + success_url = reverse_lazy('address:customer-address-list') + + def test_func(self): + result = super().test_func() + obj = self.get_object() + return result and obj.customer_user == self.request.user + + def form_valid(self, form): + instance = form.save(commit=False) + instance.user = self.request.user + instance.save() + return super().form_valid(form) + + +@method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch') +class CustomerAddressDeleteView(IsCustomer, DeleteView): + model = CustomerAddress + template_name = 'address/delete_form.html' + success_url = reverse_lazy('accounts:customer-profile') + + def test_func(self): + result = super().test_func() + obj = self.get_object() + return result and obj.customer_user == self.request.user + + +@method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch') +class CustomerAddressListView(ListView): + model = CustomerAddress + template_name = 'address/customer_address_list.html' + context_object_name = 'addresses' + + def get_queryset(self): + return super().get_queryset().filter(customer_user=self.request.user) + + +@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') +class ServiceAddressCreateView(CustomUserPasses, CreateView): + model = ServiceAddress + form_class = ServiceAddressCreateUpdateForm + template_name = 'address/create_update_form.html' + success_url = reverse_lazy('accounts:service-provider-profile') + raise_exception = True + + def setup(self, request, *args, **kwargs): + super().setup(request, *args, **kwargs) + self.service = get_object_or_404(Service, id=self.kwargs['service_pk']) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['service'] = self.service + return context + + def test_func(self): + if not isinstance(self.request.user, ServiceProvider): + return False + if self.service.service_provider != self.request.user: + return False + if self.service.address: + return False, True, reverse_lazy('accounts:service-provider-profile') + return True + + def form_valid(self, form): + with transaction.atomic(): + instance = form.save() + self.service.address = instance + self.service.save() + return super().form_valid(form) + + +@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') +class ServiceAddressUpdateView(IsServiceProvider, UpdateView): + model = ServiceAddress + form_class = ServiceAddressCreateUpdateForm + template_name = 'address/create_update_form.html' + success_url = reverse_lazy('accounts:service-provider-profile') + + def test_func(self): + result = super().test_func() + obj = self.get_object() + return result and obj.services.service_provider == self.request.user diff --git a/cart/__init__.py b/cart/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cart/admin.py b/cart/admin.py new file mode 100644 index 0000000..9db535e --- /dev/null +++ b/cart/admin.py @@ -0,0 +1,16 @@ +from django.contrib import admin + +from cart.models import Cart, CartLine + + +class CartLineInline(admin.TabularInline): + model = CartLine + extra = 0 + + +@admin.register(Cart) +class CartAdmin(admin.ModelAdmin): + list_display = ('customer', 'is_paid') + list_filter = ('is_paid',) + search_fields = ('customer__phone_number',) + inlines = [CartLineInline] diff --git a/cart/apps.py b/cart/apps.py new file mode 100644 index 0000000..f3e3ec9 --- /dev/null +++ b/cart/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CartConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'cart' diff --git a/cart/migrations/0001_initial.py b/cart/migrations/0001_initial.py new file mode 100644 index 0000000..790aa33 --- /dev/null +++ b/cart/migrations/0001_initial.py @@ -0,0 +1,54 @@ +# Generated by Django 3.2 on 2021-09-07 12:46 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('payment', '0002_alter_invoice_address'), + ('item', '0002_alter_item_upc'), + ('accounts', '0002_auto_20210831_0046'), + ('service', '0008_service_available'), + ] + + operations = [ + migrations.CreateModel( + name='Cart', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), + ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), + ('is_paid', models.BooleanField(default=False, verbose_name='is paid')), + ('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='carts', to='accounts.customer', verbose_name='customer')), + ('invoice', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='cart', to='payment.invoice', verbose_name='invoice')), + ('service', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='carts', to='service.service', verbose_name='service')), + ], + options={ + 'verbose_name': 'Cart', + 'verbose_name_plural': 'Carts', + 'db_table': 'cart', + }, + ), + migrations.CreateModel( + name='CartLine', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), + ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), + ('quantity', models.PositiveIntegerField(default=1, verbose_name='quantity')), + ('cart', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lines', to='cart.cart', verbose_name='cart')), + ('item', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='lines', to='item.item', verbose_name='item')), + ], + options={ + 'verbose_name': 'Cart line', + 'verbose_name_plural': 'Cart lines', + 'db_table': 'cart_line', + 'ordering': ('created_time', 'modified_time'), + 'unique_together': {('item', 'cart')}, + }, + ), + ] \ No newline at end of file diff --git a/cart/migrations/0002_remove_cart_invoice.py b/cart/migrations/0002_remove_cart_invoice.py new file mode 100644 index 0000000..7a60ad5 --- /dev/null +++ b/cart/migrations/0002_remove_cart_invoice.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2 on 2021-09-12 12:27 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('cart', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='cart', + name='invoice', + ), + ] diff --git a/cart/migrations/__init__.py b/cart/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cart/models.py b/cart/models.py new file mode 100644 index 0000000..b581a72 --- /dev/null +++ b/cart/models.py @@ -0,0 +1,82 @@ +from django.db import models, transaction +from django.db.models import Sum, F +from django.utils.translation import gettext as _ +from accounts.models import Customer +from item.models import Item +from library.models import BaseModel +from service.models import Service + + +class Cart(BaseModel): + customer = models.ForeignKey( + Customer, + verbose_name=_('customer'), + null=True, blank=True, + related_name='carts', + on_delete=models.CASCADE + ) + is_paid = models.BooleanField(default=False, verbose_name=_('is paid')) + service = models.ForeignKey( + Service, + verbose_name=_('service'), + related_name='carts', + on_delete=models.PROTECT, + null=True + ) + + def __str__(self): + return f"{self.customer} - {'Paid' if self.is_paid else 'Not paid'}" + + @property + def total_price(self): + return self.lines.all().annotate(price=F('quantity') * F('item__price')).aggregate( + total_price=Sum('price')).get('total_price') + + @classmethod + def get_cart(cls, cart_id): + if cart_id is None: + cart = cls.objects.create() + else: + cart, created = cls.objects.get_or_create(pk=cart_id) + return cart + + def create_or_increase(self, item): + with transaction.atomic(): + + if item.service != self.service: # adding the first item or item with different service + self.empty_cart() + self.service = item.service + self.save() + + cart_line, create = self.lines.select_for_update().get_or_create(item=item, defaults={'cart': self}) + if not create: + cart_line.quantity += 1 + cart_line.save() + + def empty_cart(self): + self.lines.all().delete() + + class Meta: + verbose_name = _('Cart') + verbose_name_plural = _('Carts') + db_table = 'cart' + + +class CartLine(BaseModel): + item = models.ForeignKey(Item, verbose_name=_('item'), related_name='lines', on_delete=models.PROTECT) + cart = models.ForeignKey(Cart, verbose_name=_('cart'), related_name='lines', on_delete=models.CASCADE) + quantity = models.PositiveIntegerField(default=1, verbose_name=_('quantity')) + + def __str__(self): + return f"{self.item} - {self.quantity}" + + @property + def price(self): + return self.item.price * self.quantity + + class Meta: + verbose_name = _('Cart line') + verbose_name_plural = _('Cart lines') + db_table = 'cart_line' + ordering = ('created_time', 'modified_time') + unique_together = ('item', 'cart') # each cart can have one cart line with the same item diff --git a/cart/templatetags/__init__.py b/cart/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cart/templatetags/cart_tags.py b/cart/templatetags/cart_tags.py new file mode 100644 index 0000000..520af02 --- /dev/null +++ b/cart/templatetags/cart_tags.py @@ -0,0 +1,25 @@ +from django import template +from django.db.models import Sum, F + + +from cart.models import Cart + +register = template.Library() + + +@register.simple_tag +def get_cart(request, service): + try: + cart = Cart.objects.get(id=request.COOKIES.get('cart_id', None), is_paid=False) + except Cart.DoesNotExist: + return None + else: + if cart.lines.filter(item__service=service).exists(): + return cart + return None + + +@register.simple_tag +def calculate_total_price(cart): + price = cart.lines.annotate(true_price=(F('quantity') * F('item__price'))).aggregate(Sum('true_price')) + return price['true_price__sum'] diff --git a/cart/tests.py b/cart/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/cart/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/cart/urls.py b/cart/urls.py new file mode 100644 index 0000000..17145e0 --- /dev/null +++ b/cart/urls.py @@ -0,0 +1,13 @@ +from django.urls import path + +from cart.views import AddToCartView, CartLineDeleteView, CartLineDecreaseView, EmptyCartView + +app_name = 'cart' + +urlpatterns = [ + path('add/', AddToCartView.as_view(), name='add-to-cart'), + path('cartline/delete//', CartLineDeleteView.as_view(), name='cart-line-delete'), + path('cartline/decrease//', CartLineDecreaseView.as_view(), name='cart-line-decrease'), + path('cart-empty/', EmptyCartView.as_view(), name='empty-cart'), + +] diff --git a/cart/views.py b/cart/views.py new file mode 100644 index 0000000..d15f740 --- /dev/null +++ b/cart/views.py @@ -0,0 +1,106 @@ +from django.http import HttpResponseRedirect, Http404 +from django.shortcuts import get_object_or_404 +from django.urls import reverse_lazy +from django.utils.decorators import method_decorator +from django.views.decorators.http import require_http_methods +from django.views.generic import View + +from accounts.models import Customer +from cart.models import Cart, CartLine +from item.models import Item + + +class BaseCartView(View): + def check_customer(self, cart): + user = self.request.user + if user.is_authenticated: + if not isinstance(user, Customer): + raise Http404 + if cart.customer is None: + cart.customer = user + cart.save() + return cart + else: + if user != cart.customer: + raise Http404 + return cart + elif cart.customer is not None: + return Cart.objects.create() + else: + return cart + + def get_cart_set_cookie(self, redirect_url=None): + response = HttpResponseRedirect(redirect_url) + cart = Cart.get_cart(self.request.COOKIES.get('cart_id', None)) + cart = self.check_customer(cart) + response.set_cookie('cart_id', cart.pk, max_age=15 * 60) + return cart, response + + +@method_decorator(require_http_methods(('POST',)), name='dispatch') +class AddToCartView(BaseCartView): + + def setup(self, request, *args, **kwargs): + super().setup(request, *args, **kwargs) + self.kwargs['item'] = get_object_or_404(Item, pk=request.POST['item_id']) + + def get_success_url(self): + return reverse_lazy('item:list', kwargs={'service_pk': self.kwargs['item'].service.pk}) + + def post(self, request, *args, **kwargs): + cart, response = self.get_cart_set_cookie(self.get_success_url()) + cart.create_or_increase(self.kwargs['item']) + return response + + +@method_decorator(require_http_methods(('POST',)), name='dispatch') +class CartLineDeleteView(BaseCartView): + model = CartLine + + def get_success_url(self): + return reverse_lazy('item:list', kwargs={'service_pk': self.kwargs['cart_line'].item.service.pk}) + + def setup(self, request, *args, **kwargs): + super().setup(request, *args, **kwargs) + self.kwargs['cart_line'] = get_object_or_404(CartLine, pk=self.kwargs['pk']) + + def post(self, request, *args, **kwargs): + cart, response = self.get_cart_set_cookie(self.get_success_url()) + if self.kwargs['cart_line'].cart != cart: + raise Http404 + self.kwargs['cart_line'].delete() + return response + + +@method_decorator(require_http_methods(('POST',)), name='dispatch') +class CartLineDecreaseView(BaseCartView): + + def setup(self, request, *args, **kwargs): + super().setup(request, *args, **kwargs) + self.kwargs['cart_line'] = get_object_or_404(CartLine, id=self.kwargs['pk']) + + def get_success_url(self): + return reverse_lazy('item:list', kwargs={'service_pk': self.kwargs['cart_line'].item.service.pk}) + + def post(self, request, *args, **kwargs): + cart, response = self.get_cart_set_cookie(self.get_success_url()) + if self.kwargs['cart_line'].cart != cart: + raise Http404 + + if self.kwargs['cart_line'].quantity >= 2: + self.kwargs['cart_line'].quantity -= 1 + self.kwargs['cart_line'].save() + + return response + + +@method_decorator(require_http_methods(('POST',)), name='dispatch') +class EmptyCartView(BaseCartView): + + def post(self, request, *args, **kwargs): + cart = Cart.get_cart(self.request.COOKIES.get('cart_id', None)) + cart = self.check_customer(cart) + cart.empty_cart() + cart.save() + response = HttpResponseRedirect(reverse_lazy('item:list', kwargs={'service_pk': cart.service.pk})) + return response diff --git a/gateway/__init__.py b/gateway/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gateway/admin.py b/gateway/admin.py new file mode 100644 index 0000000..344edbf --- /dev/null +++ b/gateway/admin.py @@ -0,0 +1,11 @@ +from django.contrib import admin + +from gateway.models import Gateway + + +@admin.register(Gateway) +class GatewayAdmin(admin.ModelAdmin): + list_display = ('title', 'gateway_code', 'is_enable') + list_filter = ('is_enable',) + search_fields = ('title',) + diff --git a/gateway/apps.py b/gateway/apps.py new file mode 100644 index 0000000..5e3f248 --- /dev/null +++ b/gateway/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class GatewayConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'gateway' diff --git a/gateway/migrations/0001_initial.py b/gateway/migrations/0001_initial.py new file mode 100644 index 0000000..246f5be --- /dev/null +++ b/gateway/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# Generated by Django 3.2 on 2021-09-12 12:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Gateway', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), + ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), + ('title', models.CharField(max_length=100, verbose_name='title')), + ('gateway_request_url', models.CharField(blank=True, max_length=150, verbose_name='request url')), + ('gateway_verify_url', models.CharField(blank=True, max_length=150, verbose_name='verify url')), + ('gateway_code', models.CharField(choices=[('zarrinpal', 'Zarrinpal'), ('saman', 'Saman')], max_length=20, verbose_name='gateway code')), + ('is_enable', models.BooleanField(default=True, verbose_name='is enable')), + ('auth_data', models.TextField(blank=True, verbose_name='auth data')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/gateway/migrations/0002_auto_20210912_2211.py b/gateway/migrations/0002_auto_20210912_2211.py new file mode 100644 index 0000000..f35215f --- /dev/null +++ b/gateway/migrations/0002_auto_20210912_2211.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2 on 2021-09-12 17:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('gateway', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='gateway', + options={'verbose_name': 'Gateway', 'verbose_name_plural': 'Gateways'}, + ), + migrations.AlterModelTable( + name='gateway', + table='gateway', + ), + ] diff --git a/gateway/migrations/__init__.py b/gateway/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gateway/models.py b/gateway/models.py new file mode 100644 index 0000000..45fa09b --- /dev/null +++ b/gateway/models.py @@ -0,0 +1,43 @@ +from django.db import models +from django.utils.translation import gettext as _ + +from gateway.utils.zarrinpal import zarrinpal_request_handler, zarrinpal_payment_verify +from library.models import BaseModel + + +class Gateway(BaseModel): + FUNCTION_ZARRINPAL = 'zarrinpal' + FUNCTION_SAMAN = 'saman' + GATEWAY_FUNCTIONS_CHOICES = ( + (FUNCTION_ZARRINPAL, _('Zarrinpal')), + (FUNCTION_SAMAN, _('Saman')) + ) + + title = models.CharField(max_length=100, verbose_name=_('title')) + gateway_request_url = models.CharField(max_length=150, verbose_name=_('request url'), blank=True) + gateway_verify_url = models.CharField(max_length=150, verbose_name=_('verify url'), blank=True) + gateway_code = models.CharField(max_length=20, verbose_name=_('gateway code'), choices=GATEWAY_FUNCTIONS_CHOICES) + is_enable = models.BooleanField(verbose_name=_('is enable'), default=True) + auth_data = models.TextField(verbose_name=_('auth data'), blank=True) + + def __str__(self): + return self.title + + def get_request_handler(self): + handlers = { + self.FUNCTION_ZARRINPAL: zarrinpal_request_handler, + self.FUNCTION_SAMAN: None, + } + return handlers[self.gateway_code] + + def get_verify_handler(self): + handlers = { + self.FUNCTION_ZARRINPAL: zarrinpal_payment_verify, + self.FUNCTION_SAMAN: None, + } + return handlers[self.gateway_code] + + class Meta: + verbose_name = _('Gateway') + verbose_name_plural = _('Gateways') + db_table = 'gateway' diff --git a/gateway/tests.py b/gateway/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/gateway/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/gateway/utils/__init__.py b/gateway/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gateway/utils/zarrinpal.py b/gateway/utils/zarrinpal.py new file mode 100644 index 0000000..fb7d910 --- /dev/null +++ b/gateway/utils/zarrinpal.py @@ -0,0 +1,19 @@ +from suds.client import Client + + +def zarrinpal_request_handler(amount, description, user_email, user_phone_number, REQUEST_URL, MERCHANT_ID, CALL_BACK): + client = Client(REQUEST_URL) + result = client.service.PaymentRequest( + MERCHANT_ID, amount, description, user_email, user_phone_number, CALL_BACK + ) + if result.Status == 100: + return 'https://sandbox.zarinpal.com/pg/StartPay/' + result.Authority, result.Authority + else: + return None, None + + +def zarrinpal_payment_verify(amount, authority, REQUEST_URL, MERCHANT_ID,): + client = Client(REQUEST_URL) + result = client.service.PaymentVerification(MERCHANT_ID, authority, amount) + is_paid = True if result.Status in (100, 101) else False + return is_paid, result.RefID diff --git a/gateway/views.py b/gateway/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/gateway/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/item/__init__.py b/item/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/item/admin.py b/item/admin.py new file mode 100644 index 0000000..c7adcca --- /dev/null +++ b/item/admin.py @@ -0,0 +1,17 @@ +from django.contrib import admin + +from item.models import Item, ItemLine + + +class ItemLineInline(admin.TabularInline): + model = ItemLine + max_num = 1 + + +@admin.register(Item) +class ItemAdmin(admin.ModelAdmin): + list_display = ('name', 'upc', 'service', 'category', 'price', 'stock', 'available') + list_editable = ('available',) + list_filter = ('available', 'created_time') + search_fields = ('upc', 'name') + inlines = [ItemLineInline] \ No newline at end of file diff --git a/item/apps.py b/item/apps.py new file mode 100644 index 0000000..f290658 --- /dev/null +++ b/item/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ItemConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'item' diff --git a/item/forms.py b/item/forms.py new file mode 100644 index 0000000..d2595db --- /dev/null +++ b/item/forms.py @@ -0,0 +1,19 @@ +from django import forms + +from item.models import Item + + +class ItemCreateForm(forms.ModelForm): + quantity = forms.IntegerField(initial=0) + + class Meta: + model = Item + fields = ('name', 'price', 'description', 'available', 'image') + + +class ItemUpdateForm(forms.ModelForm): + quantity = forms.IntegerField(initial=0) + + class Meta: + model = Item + fields = ('category', 'name', 'price', 'description', 'available', 'image') diff --git a/item/migrations/0001_initial.py b/item/migrations/0001_initial.py new file mode 100644 index 0000000..7c50bbb --- /dev/null +++ b/item/migrations/0001_initial.py @@ -0,0 +1,52 @@ +# Generated by Django 3.2 on 2021-08-28 20:31 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('service', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Item', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), + ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), + ('upc', models.BigIntegerField(db_index=True, unique=True, verbose_name='upc')), + ('available', models.BooleanField(default=True, verbose_name='available')), + ('name', models.CharField(max_length=50, verbose_name='name')), + ('description', models.TextField(blank=True, verbose_name='description')), + ('price', models.IntegerField(verbose_name='price')), + ('image', models.ImageField(blank=True, null=True, upload_to='items/', verbose_name='image')), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='service.servicecategory', verbose_name='category')), + ('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='service.service', verbose_name='service')), + ], + options={ + 'verbose_name': 'Item', + 'verbose_name_plural': 'Items', + 'db_table': 'item', + }, + ), + migrations.CreateModel( + name='ItemLine', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), + ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), + ('quantity', models.PositiveIntegerField(default=0, verbose_name='quantity')), + ('item', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='line', to='item.item', verbose_name='item')), + ], + options={ + 'verbose_name': 'Item line', + 'verbose_name_plural': 'Item lines', + 'db_table': 'item_line', + }, + ), + ] diff --git a/item/migrations/0002_alter_item_upc.py b/item/migrations/0002_alter_item_upc.py new file mode 100644 index 0000000..c09abd8 --- /dev/null +++ b/item/migrations/0002_alter_item_upc.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2 on 2021-09-01 08:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('item', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='item', + name='upc', + field=models.BigIntegerField(db_index=True, editable=False, unique=True, verbose_name='upc'), + ), + ] diff --git a/item/migrations/__init__.py b/item/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/item/models.py b/item/models.py new file mode 100644 index 0000000..c431000 --- /dev/null +++ b/item/models.py @@ -0,0 +1,62 @@ +import random +import string + +from django.db import models +from django.urls import reverse_lazy +from django.utils.translation import gettext as _ + +from library.models import BaseModel +from service.models import Service, ServiceCategory + + +class ItemManager(models.Manager): + def available(self): + return self.get_queryset().filter(available=True) + + +class Item(BaseModel): + upc = models.BigIntegerField(verbose_name=_('upc'), unique=True, db_index=True, editable=False) + available = models.BooleanField(verbose_name=_('available'), default=True) + name = models.CharField(verbose_name=_('name'), max_length=50) + description = models.TextField(verbose_name=_('description'), blank=True) + price = models.IntegerField(verbose_name=_('price')) + image = models.ImageField(verbose_name=_('image'), blank=True, null=True, upload_to='items/') + service = models.ForeignKey(Service, verbose_name=_('service'), related_name='items', on_delete=models.CASCADE) + category = models.ForeignKey(ServiceCategory, verbose_name=_('category'), related_name='items', + on_delete=models.CASCADE) + objects = ItemManager() + + class Meta: + verbose_name = _('Item') + verbose_name_plural = _('Items') + db_table = 'item' + + @property + def stock(self): + return self.line.quantity + + def __str__(self): + return self.name + + def save(self, force_insert=False, force_update=False, using=None, + update_fields=None): + if self.pk is None: # only when we the instance is created + random_digits = ''.join(random.choice(string.digits) for _ in range(5)) + self.upc = int(random_digits) + return super().save(force_insert, force_update, using, update_fields) + + def get_absolute_url(self): + return reverse_lazy("item:detail", kwargs={'pk': self.pk}) + + +class ItemLine(BaseModel): + item = models.OneToOneField(Item, verbose_name=_('item'), related_name='line', on_delete=models.CASCADE) + quantity = models.PositiveIntegerField(default=0, verbose_name=_('quantity')) + + class Meta: + verbose_name = _('Item line') + verbose_name_plural = _('Item lines') + db_table = 'item_line' + + def __str__(self): + return f"{self.quantity}" diff --git a/item/templates/item/create_form.html b/item/templates/item/create_form.html new file mode 100644 index 0000000..4a7f9b9 --- /dev/null +++ b/item/templates/item/create_form.html @@ -0,0 +1,22 @@ +{% extends 'base.html' %} +{% block title %} Add item {% endblock %} +{% block content %} +

Add new item

+
+ {% csrf_token %} + {% for field in form %} + +
+

{{ field }}

+ {% for error in field.errors %} +

{{ error }}

+ {% endfor %} + {% endfor %} +
+ {% if form.errors %} + {% block form_error %} {% endblock %} + {% endif %} + + +
+{% endblock %} \ No newline at end of file diff --git a/item/templates/item/delete.html b/item/templates/item/delete.html new file mode 100644 index 0000000..8e62538 --- /dev/null +++ b/item/templates/item/delete.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% block content %} +
+
+ {% csrf_token %} +

Are you sure to delete this item from your menu?

+

{{ object.name }}

+ + +
+
+ Cancel +
+{% endblock %} diff --git a/item/templates/item/detail.html b/item/templates/item/detail.html new file mode 100644 index 0000000..2773c38 --- /dev/null +++ b/item/templates/item/detail.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% block title %} {{ item.name }} {% endblock %} +{% block content %} +

{{ item.name }}

+

Category : {{ item.category.name }}

+ {% if item.image %} + {{ item.name }} image + {% endif %} +

Description : {{ item.description }}

+

Price : {{ item.price }}

+ Add to cart

+ Back to List +{% endblock %} \ No newline at end of file diff --git a/item/templates/item/item_list.html b/item/templates/item/item_list.html new file mode 100644 index 0000000..37d1e3b --- /dev/null +++ b/item/templates/item/item_list.html @@ -0,0 +1,194 @@ +{% extends 'base.html' %} +{% load category_tags cart_tags %} +{% block banner %} +
+
+
+ {% if service.banner %} + + {% else %} + + {% endif %} +
+
+
+ {% if service.logo %} + + {% else %} + + {% endif %} +
+
+

{{ service.name.title }} {{ service.get_service_type_display.title }}

+

Address : {{ service.address }}

+
+
+
+
+{% endblock %} +{% block content %} +
+ +
+ {% for category in service.categories.all %} +
+

{{ category.name.title }}

+ {% filter_category items category as category_items %} + {% for item in category_items %} +
+
+
+ {% if item.image %} + {{ item.name }} image + {% else %} + + {% endif %} +
+
+
+ + {{ item.name }} + +
+

{{ item.description | truncatechars:35 }}

+
+
+ +
+ {% csrf_token %} + +
+ +
+
+ {{ item.price }} $ +
+
+
+
+
+ {% endfor %} +
+ {% endfor %} +
+
+ {% get_cart request service as cart %} + {% if cart %} +
+ +
+
Cart
+
+
+
+ {% csrf_token %} + +
+
+ + +
+ +
+ {% calculate_total_price cart as total_price %} + +
Total price
+
{{ total_price }} $
+
+
+ + check out + {% else %} +
+ +
+
Cart is empty! +
+
+
+ {% endif %} + + +
+
+ +{% endblock %} diff --git a/item/templates/item/service_provider_detail.html b/item/templates/item/service_provider_detail.html new file mode 100644 index 0000000..df0282b --- /dev/null +++ b/item/templates/item/service_provider_detail.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% block title %} {{ item.name }} {% endblock %} +{% block content %} +

{{ item.name }}

+ {% if item.image %} + {{ item.name }} image + {% endif %} +

Stock : {{ item.stock }}

+

Category : {{ item.category.name }}

+

Description : {{ item.description }}

+

Price : {{ item.price }}


+ Back to List +{% endblock %} \ No newline at end of file diff --git a/item/templates/item/service_provider_item_list.html b/item/templates/item/service_provider_item_list.html new file mode 100644 index 0000000..64fe1a6 --- /dev/null +++ b/item/templates/item/service_provider_item_list.html @@ -0,0 +1,38 @@ +{% extends 'base.html' %} + +{% block content %} +

Category Detail

+
Category Name : {{ category.name }}
+
+
Items
+
+ Add item +

+
+ {% for item in items %} +
+
+
+ {% if item.image %} + {{ item.name }} image + {% else %} + + {% endif %} +
{{ item.name }}

+ +
Price : {{ item.price }}
+

{{ item.description | truncatechars:30 }}

+ Detail + Update + Delete +
+
+
+ {% endfor %} +
+ +{% endblock %} \ No newline at end of file diff --git a/item/templates/item/update_form.html b/item/templates/item/update_form.html new file mode 100644 index 0000000..527caf6 --- /dev/null +++ b/item/templates/item/update_form.html @@ -0,0 +1,22 @@ +{% extends 'base.html' %} +{% block title %} Update Item {% endblock %} +{% block content %} +

Update item

+
+ {% csrf_token %} + {% for field in form %} + +
+

{{ field }}

+ {% for error in field.errors %} +

{{ error }}

+ {% endfor %} + {% endfor %} +
+ {% if form.errors %} + {% block form_error %} {% endblock %} + {% endif %} + + +
+{% endblock %} \ No newline at end of file diff --git a/item/templatetags/__init__.py b/item/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/item/templatetags/category_tags.py b/item/templatetags/category_tags.py new file mode 100644 index 0000000..b51e656 --- /dev/null +++ b/item/templatetags/category_tags.py @@ -0,0 +1,8 @@ +from django import template + +register = template.Library() + + +@register.simple_tag +def filter_category(queryset, category): + return queryset.filter(category=category) diff --git a/item/tests.py b/item/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/item/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/item/urls.py b/item/urls.py new file mode 100644 index 0000000..42557e7 --- /dev/null +++ b/item/urls.py @@ -0,0 +1,17 @@ +from django.urls import path +from item.views import ItemUpdateView, ItemDetailView, ItemDeleteView, ServiceProviderItemDetailView, ItemCreateView, \ + ItemListView, ServiceCategoryItemListView + +app_name = 'item' + +urlpatterns = [ + path('create///', ItemCreateView.as_view(), name='create'), + path('update//', ItemUpdateView.as_view(), name='update'), + path('detail//', ItemDetailView.as_view(), name='detail'), + path('detail-serviceprovider//', ServiceProviderItemDetailView.as_view(), name='detail-service-provider'), + path('delete//', ItemDeleteView.as_view(), name='delete'), + path('/list/', ItemListView.as_view(), name='list'), + path( + 'serviceprovider///list/', ServiceCategoryItemListView.as_view(), + name='service-provider-list'), +] diff --git a/item/views.py b/item/views.py new file mode 100644 index 0000000..2e57885 --- /dev/null +++ b/item/views.py @@ -0,0 +1,156 @@ +from django.contrib.auth.decorators import login_required +from django.shortcuts import get_object_or_404 +from django.urls import reverse_lazy +from django.utils.decorators import method_decorator +from django.views.generic import FormView, UpdateView, DetailView, DeleteView, ListView + +from accounts.utils import IsServiceProvider +from item.forms import ItemCreateForm, ItemUpdateForm +from item.models import Item, ItemLine +from service.models import Service, ServiceCategory + + +@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') +class ItemCreateView(IsServiceProvider, FormView): + model = Item + form_class = ItemCreateForm + template_name = 'item/create_form.html' + + def setup(self, request, *args, **kwargs): + super().setup(request, *args, **kwargs) + self.kwargs['service'] = get_object_or_404(Service, id=self.kwargs['service_pk']) + self.kwargs['category'] = get_object_or_404(ServiceCategory, id=self.kwargs['category_pk']) + + def form_valid(self, form): + item = form.save(commit=False) + item.service = self.kwargs['service'] + item.category = self.kwargs['category'] + item.save() + ItemLine.objects.create(item=item, quantity=form.cleaned_data['quantity']) + item.save() + return super().form_valid(form) + + def get_success_url(self): + return reverse_lazy('item:service-provider-list', + kwargs={'service_pk': self.kwargs['service'].pk, 'category_pk': self.kwargs['category'].pk} + ) + + def test_func(self): + result = super().test_func() + service_check = self.kwargs['service'].service_provider == self.request.user + return result and service_check + + +@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') +class ItemUpdateView(IsServiceProvider, UpdateView): + model = Item + template_name = 'item/update_form.html' + form_class = ItemUpdateForm + + def get_initial(self): + initial = super().get_initial() + initial['quantity'] = self.get_object().stock + return initial + + def get_form_class(self): + form = super().get_form_class() + queryset = form.base_fields['category'].queryset + form.base_fields['category'].queryset = queryset.filter(service=self.get_object().service) + return form + + def form_valid(self, form): + item = form.save(commit=False) + item.line.quantity = form.cleaned_data['quantity'] + item.line.save() + item.save() + return super().form_valid(form) + + def get_success_url(self): + return reverse_lazy('item:detail-service-provider', kwargs={'pk': self.object.pk}) + + def test_func(self): + result = super().test_func() + return result and self.get_object().service.service_provider == self.request.user + + +class ItemDetailView(DetailView): + model = Item + template_name = 'item/detail.html' + context_object_name = 'item' + + def get_queryset(self): + return Item.objects.available() + + +@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') +class ServiceProviderItemDetailView(IsServiceProvider, DetailView): + model = Item + template_name = 'item/service_provider_detail.html' + context_object_name = 'item' + + def test_func(self): + result = super().test_func() + return result and self.get_object().service.service_provider == self.request.user + + +@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') +class ItemDeleteView(IsServiceProvider, DeleteView): + model = Item + template_name = 'item/delete.html' + + def test_func(self): + result = super().test_func() + return result and self.get_object().service.service_provider == self.request.user + + def get_success_url(self): + return reverse_lazy('item:service-provider-list', + kwargs={'service_pk': self.object.service.pk, 'category_pk': self.object.category.pk} + ) + + +class BaseItemListView(ListView): + model = Item + context_object_name = 'items' + + +class ItemListView(BaseItemListView): + template_name = 'item/item_list.html' + + def setup(self, request, *args, **kwargs): + super().setup(request, *args, **kwargs) + self.kwargs['service'] = get_object_or_404(Service, id=self.kwargs['service_pk']) + + def get_queryset(self): + return Item.objects.available().filter(service=self.kwargs['service']) + + def get_context_data(self, *, object_list=None, **kwargs): + context = super().get_context_data(object_list=None, **kwargs) + context['service'] = self.kwargs['service'] + return context + + +@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') +class ServiceCategoryItemListView(IsServiceProvider, BaseItemListView): + """ + This view is for service-provider panel + """ + template_name = 'item/service_provider_item_list.html' + + def setup(self, request, *args, **kwargs): + super().setup(request, *args, **kwargs) + self.kwargs['service'] = get_object_or_404(Service, id=self.kwargs['service_pk']) + self.kwargs['category'] = get_object_or_404(ServiceCategory, id=self.kwargs['category_pk'], + service=self.kwargs['service']) + + def get_queryset(self): + return Item.objects.filter(service=self.kwargs['service'], category=self.kwargs['category']) + + def test_func(self): + result = super().test_func() + service_check = self.kwargs['service'].service_provider == self.request.user + return result and service_check + + def get_context_data(self, *, object_list=None, **kwargs): + context = super().get_context_data(object_list=None, **kwargs) + context['category'] = self.kwargs['category'] + return context diff --git a/library/__init__.py b/library/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/library/models.py b/library/models.py new file mode 100644 index 0000000..c3fe5f6 --- /dev/null +++ b/library/models.py @@ -0,0 +1,9 @@ +from django.db import models + + +class BaseModel(models.Model): + created_time = models.DateTimeField(verbose_name='created time', auto_now_add=True) + modified_time = models.DateTimeField(verbose_name='modified time', auto_now=True) + + class Meta: + abstract = True diff --git a/library/utils.py b/library/utils.py new file mode 100644 index 0000000..98b8df5 --- /dev/null +++ b/library/utils.py @@ -0,0 +1,28 @@ +from abc import ABC + +from django.contrib.auth.mixins import UserPassesTestMixin +from django.core.exceptions import PermissionDenied +from django.http import HttpResponseRedirect + + +class CustomUserPasses(ABC, UserPassesTestMixin): + raise_exception = True + + def handle_no_permission(self, path_url=None, redirect=False): + if redirect: + return HttpResponseRedirect(path_url) + + else: + raise PermissionDenied(self.get_permission_denied_message()) + + def dispatch(self, request, *args, **kwargs): + user_test_result = self.test_func() + + if isinstance(self.test_func(), tuple): + user_test_bool, redirect, path_url = user_test_result + if not user_test_bool: + return self.handle_no_permission(path_url, redirect) + + if not user_test_result: + return self.handle_no_permission() + return super().dispatch(request, *args, **kwargs) diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..d465c22 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'yummy.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/order/__init__.py b/order/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/order/admin.py b/order/admin.py new file mode 100644 index 0000000..e78a395 --- /dev/null +++ b/order/admin.py @@ -0,0 +1,11 @@ +from django.contrib import admin + +from .models import Order + + +@admin.register(Order) +class OrderAdmin(admin.ModelAdmin): + list_display = ('customer', 'status') + list_filter = ('status',) + list_editable = ('status',) + search_fields = ('customer__phone_number',) diff --git a/order/apps.py b/order/apps.py new file mode 100644 index 0000000..42888e4 --- /dev/null +++ b/order/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class OrderConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'order' diff --git a/order/filters.py b/order/filters.py new file mode 100644 index 0000000..12d04cc --- /dev/null +++ b/order/filters.py @@ -0,0 +1,17 @@ +from django import forms + +import django_filters + +from order.models import Order + + +class OrderFilter(django_filters.FilterSet): + created_time = django_filters.DateFilter( + widget=forms.DateInput(attrs={'class': 'form-control', 'type': 'date', 'data-date-format': 'YYYY-MMMM-DD '}), + lookup_expr='contains' + ) + + status = django_filters.ChoiceFilter( + choices=Order.STATUS, + widget=forms.Select(attrs={'class': 'form-control'}) + ) \ No newline at end of file diff --git a/order/migrations/0001_initial.py b/order/migrations/0001_initial.py new file mode 100644 index 0000000..a389fbb --- /dev/null +++ b/order/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# Generated by Django 3.2 on 2021-08-28 20:31 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('payment', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Order', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), + ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), + ('status', models.PositiveSmallIntegerField(choices=[(0, 'preparing food'), (0, 'sending')], verbose_name='status')), + ('is_delivered', models.BooleanField(default=False, verbose_name='is delivered')), + ('invoice', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='orders', to='payment.invoice', verbose_name='invoice')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/order/migrations/0002_auto_20210913_1544.py b/order/migrations/0002_auto_20210913_1544.py new file mode 100644 index 0000000..9743ecf --- /dev/null +++ b/order/migrations/0002_auto_20210913_1544.py @@ -0,0 +1,40 @@ +# Generated by Django 3.2 on 2021-09-13 11:14 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0002_auto_20210831_0046'), + ('payment', '0003_auto_20210912_1657'), + ('order', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='order', + options={'verbose_name': 'Order', 'verbose_name_plural': 'Orders'}, + ), + migrations.AddField( + model_name='order', + name='customer', + field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.PROTECT, related_name='orders', to='accounts.customer', verbose_name='customer'), + preserve_default=False, + ), + migrations.AlterField( + model_name='order', + name='invoice', + field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='order', to='payment.invoice', verbose_name='invoice'), + ), + migrations.AlterField( + model_name='order', + name='status', + field=models.PositiveSmallIntegerField(choices=[(0, 'preparing food'), (0, 'sending')], default=0, verbose_name='status'), + ), + migrations.AlterModelTable( + name='order', + table='order', + ), + ] diff --git a/order/migrations/0003_alter_order_status.py b/order/migrations/0003_alter_order_status.py new file mode 100644 index 0000000..cf7f259 --- /dev/null +++ b/order/migrations/0003_alter_order_status.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2 on 2021-09-15 09:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0002_auto_20210913_1544'), + ] + + operations = [ + migrations.AlterField( + model_name='order', + name='status', + field=models.PositiveSmallIntegerField(choices=[(0, 'preparing food'), (1, 'sending')], default=0, verbose_name='status'), + ), + ] diff --git a/order/migrations/0003_auto_20210913_1956.py b/order/migrations/0003_auto_20210913_1956.py new file mode 100644 index 0000000..40915c8 --- /dev/null +++ b/order/migrations/0003_auto_20210913_1956.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2 on 2021-09-13 15:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0002_auto_20210913_1544'), + ] + + operations = [ + migrations.RemoveField( + model_name='order', + name='is_delivered', + ), + migrations.AlterField( + model_name='order', + name='status', + field=models.PositiveSmallIntegerField(choices=[(0, 'preparing food'), (0, 'sending'), (2, 'delivered')], default=0, verbose_name='status'), + ), + ] diff --git a/order/migrations/0004_alter_order_status.py b/order/migrations/0004_alter_order_status.py new file mode 100644 index 0000000..b345535 --- /dev/null +++ b/order/migrations/0004_alter_order_status.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2 on 2021-09-13 15:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0003_auto_20210913_1956'), + ] + + operations = [ + migrations.AlterField( + model_name='order', + name='status', + field=models.PositiveSmallIntegerField(choices=[(0, 'preparing food'), (2, 'sending'), (2, 'delivered')], default=0, verbose_name='status'), + ), + ] diff --git a/order/migrations/0005_alter_order_status.py b/order/migrations/0005_alter_order_status.py new file mode 100644 index 0000000..7a4bb6a --- /dev/null +++ b/order/migrations/0005_alter_order_status.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2 on 2021-09-13 15:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0004_alter_order_status'), + ] + + operations = [ + migrations.AlterField( + model_name='order', + name='status', + field=models.PositiveSmallIntegerField(choices=[(0, 'preparing food'), (1, 'sending'), (2, 'delivered')], default=0, verbose_name='status'), + ), + ] diff --git a/order/migrations/0006_merge_0003_alter_order_status_0005_alter_order_status.py b/order/migrations/0006_merge_0003_alter_order_status_0005_alter_order_status.py new file mode 100644 index 0000000..d0217ad --- /dev/null +++ b/order/migrations/0006_merge_0003_alter_order_status_0005_alter_order_status.py @@ -0,0 +1,14 @@ +# Generated by Django 3.2 on 2021-09-16 08:59 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0003_alter_order_status'), + ('order', '0005_alter_order_status'), + ] + + operations = [ + ] diff --git a/order/migrations/__init__.py b/order/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/order/models.py b/order/models.py new file mode 100644 index 0000000..2079af6 --- /dev/null +++ b/order/models.py @@ -0,0 +1,33 @@ +from django.db import models + +from accounts.models import Customer +from library.models import BaseModel +from django.utils.translation import ugettext_lazy as _ + +from payment.models import Invoice + + +class Order(BaseModel): + PREPARING_FOOD = 0 + SENDING = 1 + DELIVERED = 2 + STATUS = ( + (PREPARING_FOOD, _('preparing food')), + (SENDING, _('sending')), + (DELIVERED, _('delivered')) + ) + invoice = models.OneToOneField(Invoice, verbose_name=_('invoice'), related_name='order', on_delete=models.PROTECT) + customer = models.ForeignKey(Customer, verbose_name=_('customer'), related_name='orders', on_delete=models.PROTECT) + status = models.PositiveSmallIntegerField(verbose_name=_('status'), choices=STATUS, default=PREPARING_FOOD) + + class Meta: + verbose_name = _('Order') + verbose_name_plural = _('Orders') + db_table = 'order' + + def __str__(self): + return f"{self.customer} - {self.status}" + + @classmethod + def create(cls, invoice): + cls.objects.create(invoice=invoice, customer=invoice.customer) diff --git a/order/templates/order/customer/order_detail.html b/order/templates/order/customer/order_detail.html new file mode 100644 index 0000000..d305132 --- /dev/null +++ b/order/templates/order/customer/order_detail.html @@ -0,0 +1,38 @@ +{% extends 'base.html' %} +{% block title %} Order Detail {% endblock %} + +{% block content %} +

{{ order.invoice.cart.service }}

+

+

status : {{ order.get_status_display }}

+

order date time : {{ order.created_time }}

+

address : {{ order.invoice.address.state.name }} - {{ order.invoice.address.city.name }} + - {{ order.invoice.address.area.name }} - {{ order.invoice.address.street }} + - {{ order.invoice.address.alley }} - {{ order.invoice.address.floor }} + - {{ order.invoice.address.plaque }} +

+

+ + + + + + + + + + + {% for line in order.invoice.cart.lines.all %} + + + + + + {% endfor %} + + +
ItemQuantityPrice
{{ line.item }}{{ line.quantity }}$ {{ line.price }}
+ +
Total price : {{ order.invoice.price }}
+ +{% endblock %} \ No newline at end of file diff --git a/order/templates/order/customer/order_list.html b/order/templates/order/customer/order_list.html new file mode 100644 index 0000000..3320957 --- /dev/null +++ b/order/templates/order/customer/order_list.html @@ -0,0 +1,45 @@ +{% extends 'base.html' %} +{% block title %} Orders {% endblock %} + +{% block content %} +

Active orders

+ {% if active_orders %} + + {% else %} +
+
You have no active orders :(
+ {% endif %} +

Delivered orders

+ {% if delivered_orders %} + + {% else %} +
+
You have no delivered orders
+ {% endif %} +{% endblock %} \ No newline at end of file diff --git a/order/templates/order/service/order_detail.html b/order/templates/order/service/order_detail.html new file mode 100644 index 0000000..1536d92 --- /dev/null +++ b/order/templates/order/service/order_detail.html @@ -0,0 +1,55 @@ +{% extends 'base.html' %} +{% block title %}Order Detail{% endblock %} +{% block content %} + +
+
+
+
+

id : {{ order.id }}

+

status : {{ order.get_status_display }}

+

date time : {{ order.created_time }}

+

total price : {{ order.invoice.price }}$

+
+
+ +
+

Lines:

+
+
+
+

+ Item +

+
+
+

+ Quantity +

+
+
+ {% for cartline in order.invoice.cart.lines.all %} +
+
+
+

+ {{ cartline.item }} +

+
+
+

+ {{ cartline.quantity }} +

+
+
+
+ {% endfor %} +
+ +
+{% endblock %} \ No newline at end of file diff --git a/order/templates/order/service/order_filter_list.html b/order/templates/order/service/order_filter_list.html new file mode 100644 index 0000000..6701b03 --- /dev/null +++ b/order/templates/order/service/order_filter_list.html @@ -0,0 +1,34 @@ +{% extends 'base.html' %} +{% block title %}Payment Verification{% endblock %} +{% block content %} + +
+
+
+ {{ filter.form.as_p }} + +
+
+
+ {% for order in orders %} +
+
+
+
+

id : {{ order.id }}

+

status : {{ order.get_status_display }}

+

date time : {{ order.created_time }}

+

total price : {{ order.invoice.price }}$

+
+
+ +
+
+ {% endfor %} +
+
+ +{% endblock %} diff --git a/order/templates/order/service/order_list.html b/order/templates/order/service/order_list.html new file mode 100644 index 0000000..b603793 --- /dev/null +++ b/order/templates/order/service/order_list.html @@ -0,0 +1,26 @@ +{% extends 'base.html' %} +{% block title %}Payment Verification{% endblock %} +{% block content %} +
Today: {% now "Y-m-d" %}
+
+ {% for order in orders %} +
+
+
+
+

id : {{ order.id }}

+

status : {{ order.get_status_display }}

+

date time : {{ order.created_time }}

+

total price : {{ order.invoice.price }}$

+
+
+ +
+
+ {% endfor %} +
+ +{% endblock %} \ No newline at end of file diff --git a/order/templates/order/service/order_update.html b/order/templates/order/service/order_update.html new file mode 100644 index 0000000..2d4f93e --- /dev/null +++ b/order/templates/order/service/order_update.html @@ -0,0 +1,9 @@ +{% extends 'base.html' %} +{% block content %} +
+ {% csrf_token %} + {{ form.as_p }} + +
+ cancel +{% endblock %} diff --git a/order/tests.py b/order/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/order/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/order/urls.py b/order/urls.py new file mode 100644 index 0000000..f1a4e3e --- /dev/null +++ b/order/urls.py @@ -0,0 +1,16 @@ +from django.urls import path + +from order.views import OrderServiceListView, OrderServiceDetailView, OrderServiceUpdateView, OrderServiceListFilterView +from order.views import CustomerOrdersListView, CustomerOrderDetailView + +app_name = 'order' + +urlpatterns = [ + path('service/list//', OrderServiceListFilterView.as_view(), name='service-order-list'), + path('service/today//', OrderServiceListView.as_view(), name='service-order-today'), + path('service/detail//', OrderServiceDetailView.as_view(), name='service-order-detail'), + path('service/update//', OrderServiceUpdateView.as_view(), name='service-order-update'), + + path('customer/list/', CustomerOrdersListView.as_view(), name='customer-list'), + path('customer//detail/', CustomerOrderDetailView.as_view(), name='customer-detail'), +] \ No newline at end of file diff --git a/order/views.py b/order/views.py new file mode 100644 index 0000000..ce952aa --- /dev/null +++ b/order/views.py @@ -0,0 +1,130 @@ +from django.contrib.auth.decorators import login_required +from django.http import HttpResponseForbidden +from django.shortcuts import get_object_or_404 +from django.urls import reverse_lazy +from django.utils import timezone +from django.utils.decorators import method_decorator +from django.views.decorators.http import require_http_methods +from django.views.generic import ListView, DetailView, UpdateView + +from django_filters.views import FilterView + +from order.filters import OrderFilter +from order.models import Order +from service.models import Service + +from library.utils import CustomUserPasses + +from accounts.models import Customer, ServiceProvider +from accounts.utils import IsCustomer + + +class BaseOrderServiceList(CustomUserPasses): + model = Order + + def test_func(self): + if not isinstance(self.request.user, ServiceProvider): + return False + if self.service.service_provider != self.request.user: + return False + return True + + +class BaseOrderDetailUpdate(CustomUserPasses): + model = Order + + def test_func(self): + order = self.get_object() + if not isinstance(self.reqeust.user, ServiceProvider): + return False + if order.invoice.cart.service.service_provider != self.request.user: + return False + return True + + +@method_decorator(require_http_methods(['GET']), name='dispatch') +@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') +class OrderServiceListFilterView(FilterView): + context_object_name = 'orders' + filterset_class = OrderFilter + template_name = 'order/service/order_filter_list.html' + + def dispatch(self, request, *args, **kwargs): + self.service = get_object_or_404(Service, pk=self.kwargs.get('service_pk', None)) + if self.service.service_provider != self.request.user: + return HttpResponseForbidden() + return super().dispatch(request, *args, **kwargs) + + def get_queryset(self): + return Order.objects.filter(invoice__cart__service=self.service) + + +@method_decorator(require_http_methods(['GET']), name='dispatch') +@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') +class OrderServiceListView(BaseOrderServiceList, ListView): + context_object_name = 'orders' + template_name = 'order/service/order_list.html' + + def dispatch(self, request, *args, **kwargs): + self.service = get_object_or_404(Service, pk=self.kwargs.get('service_pk', None)) + return super().dispatch(request, *args, **kwargs) + + def get_queryset(self): + today = timezone.now() + time_filter = dict( + created_time__day=today.strftime('%d'), + created_time__month=today.strftime('%m'), + created_time__year=today.strftime('%Y') + ) + return Order.objects.exclude(is_delivered=True).filter(invoice__cart__service=self.service, **time_filter) + + +@method_decorator(require_http_methods(['GET']), name='dispatch') +@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') +class OrderServiceDetailView(BaseOrderDetailUpdate, DetailView): + context_object_name = 'order' + template_name = 'order/service/order_detail.html' + + +@method_decorator(require_http_methods(['GET', 'POST']), name='dispatch') +@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') +class OrderServiceUpdateView(BaseOrderDetailUpdate, UpdateView): + fields = ('status',) + context_object_name = 'order' + template_name = 'order/service/order_update.html' + + def get_success_url(self): + order = self.get_object() + return reverse_lazy('order:service-order-list', kwargs={'service_pk': order.invoice.cart.service.id}) + + +@method_decorator(require_http_methods(['GET']), name='dispatch') +@method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch') +class CustomerOrdersListView(IsCustomer, ListView): + template_name = 'order/customer/order_list.html' + context_object_name = 'active_orders' + + def get_queryset(self): + return Order.objects.select_related('invoice__cart').filter(customer=self.request.user, status__in=(0, 1)) + + def get_context_data(self, *, object_list=None, **kwargs): + context = super().get_context_data(object_list=None, **kwargs) + context['delivered_orders'] = Order.objects.select_related('invoice__cart').filter(customer=self.request.user, + status=2) + return context + + +@method_decorator(require_http_methods(['GET']), name='dispatch') +@method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch') +class CustomerOrderDetailView(CustomUserPasses, DetailView): + model = Order + template_name = 'order/customer/order_detail.html' + context_object_name = 'order' + + def test_func(self): + user = self.request.user + if not isinstance(user, Customer): + return False + if self.get_object().customer != self.request.user: + return False + return True diff --git a/payment/__init__.py b/payment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/payment/admin.py b/payment/admin.py new file mode 100644 index 0000000..d8e582e --- /dev/null +++ b/payment/admin.py @@ -0,0 +1,17 @@ +from django.contrib import admin + +from payment.models import Invoice, Payment + + +@admin.register(Invoice) +class InvoiceAdmin(admin.ModelAdmin): + list_display = ('customer', 'price', 'is_paid', 'address') + list_filter = ('is_paid', 'created_time') + search_fields = ('customer__phone_number',) + + +@admin.register(Payment) +class PaymentAdmin(admin.ModelAdmin): + list_display = ('uuid', 'customer', 'gateway', 'price', 'is_paid') + list_filter = ('is_paid', 'created_time', 'gateway') + search_fields = ('uuid', 'customer__phone_number') diff --git a/payment/apps.py b/payment/apps.py new file mode 100644 index 0000000..b4a45c3 --- /dev/null +++ b/payment/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PaymentConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'payment' diff --git a/payment/forms.py b/payment/forms.py new file mode 100644 index 0000000..f348305 --- /dev/null +++ b/payment/forms.py @@ -0,0 +1,12 @@ +from django import forms + +from address.models import CustomerAddress +from gateway.models import Gateway + + +class AddressSelectForm(forms.Form): + address = forms.ModelChoiceField(queryset=CustomerAddress.objects.all(), widget=forms.RadioSelect) + + +class GatewaySelectForm(forms.Form): + gateway = forms.ModelChoiceField(queryset=Gateway.objects.filter(is_enable=True), widget=forms.RadioSelect) diff --git a/payment/migrations/0001_initial.py b/payment/migrations/0001_initial.py new file mode 100644 index 0000000..a281ac2 --- /dev/null +++ b/payment/migrations/0001_initial.py @@ -0,0 +1,52 @@ +# Generated by Django 3.2 on 2021-08-28 20:31 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('accounts', '0001_initial'), + ('address', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Invoice', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), + ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), + ('price', models.IntegerField(verbose_name='price')), + ('is_paid', models.BooleanField(default=False, verbose_name='is paid')), + ('address', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoices', to='address.address', verbose_name='address')), + ('customer', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoices', to='accounts.customer', verbose_name='customer')), + ], + options={ + 'verbose_name': 'Invoice', + 'verbose_name_plural': 'Invoices', + 'db_table': 'invoice', + }, + ), + migrations.CreateModel( + name='Payment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), + ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), + ('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True, verbose_name='uuid')), + ('price', models.IntegerField(verbose_name='price')), + ('is_paid', models.BooleanField(default=False, verbose_name='is paid')), + ('invoice', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='payment', to='payment.invoice', verbose_name='invoice')), + ], + options={ + 'verbose_name': 'Payment', + 'verbose_name_plural': 'Payments', + 'db_table': 'payment', + }, + ), + ] diff --git a/payment/migrations/0002_alter_invoice_address.py b/payment/migrations/0002_alter_invoice_address.py new file mode 100644 index 0000000..09765ea --- /dev/null +++ b/payment/migrations/0002_alter_invoice_address.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2 on 2021-08-30 08:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('address', '0002_auto_20210830_1305'), + ('payment', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='invoice', + name='address', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoices', to='address.customeraddress', verbose_name='address'), + ), + ] diff --git a/payment/migrations/0003_auto_20210912_1657.py b/payment/migrations/0003_auto_20210912_1657.py new file mode 100644 index 0000000..f28c2d5 --- /dev/null +++ b/payment/migrations/0003_auto_20210912_1657.py @@ -0,0 +1,54 @@ +# Generated by Django 3.2 on 2021-09-12 12:27 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('address', '0006_rename_user_customeraddress_customer_user'), + ('gateway', '0001_initial'), + ('cart', '0002_remove_cart_invoice'), + ('accounts', '0002_auto_20210831_0046'), + ('payment', '0002_alter_invoice_address'), + ] + + operations = [ + migrations.AddField( + model_name='invoice', + name='cart', + field=models.OneToOneField(default=None, on_delete=django.db.models.deletion.PROTECT, related_name='invoice', to='cart.cart', verbose_name='cart'), + preserve_default=False, + ), + migrations.AddField( + model_name='payment', + name='authority', + field=models.CharField(blank=True, max_length=64, verbose_name='authority'), + ), + migrations.AddField( + model_name='payment', + name='customer', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='payments', to='accounts.customer', verbose_name='customer'), + ), + migrations.AddField( + model_name='payment', + name='gateway', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='payments', to='gateway.gateway', verbose_name='gateway'), + ), + migrations.AddField( + model_name='payment', + name='payment_log', + field=models.TextField(blank=True, verbose_name='logs'), + ), + migrations.AlterField( + model_name='invoice', + name='address', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoices', to='address.customeraddress', verbose_name='address'), + ), + migrations.AlterField( + model_name='payment', + name='invoice', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payment', to='payment.invoice', verbose_name='invoice'), + ), + ] diff --git a/payment/migrations/__init__.py b/payment/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/payment/models.py b/payment/models.py new file mode 100644 index 0000000..86299c9 --- /dev/null +++ b/payment/models.py @@ -0,0 +1,104 @@ +from django.db import models, transaction +from django.utils.translation import gettext as _ +from accounts.models import Customer +from address.models import CustomerAddress +from cart.models import Cart +from gateway.models import Gateway +from library.models import BaseModel +import uuid + +from yummy.settings import CALL_BACK + + +class Invoice(BaseModel): + customer = models.ForeignKey(Customer, verbose_name=_('customer'), related_name='invoices', + on_delete=models.SET_NULL, null=True) + price = models.IntegerField(verbose_name=_('price')) + is_paid = models.BooleanField(verbose_name=_('is paid'), default=False) + cart = models.OneToOneField(Cart, verbose_name=_('cart'), related_name='invoice', on_delete=models.PROTECT) + address = models.ForeignKey(CustomerAddress, verbose_name=_('address'), related_name='invoices', + on_delete=models.SET_NULL, null=True) + + class Meta: + verbose_name = _('Invoice') + verbose_name_plural = _('Invoices') + db_table = 'invoice' + + @classmethod + def create_payment(cls, invoice, gateway): + return Payment.objects.create(invoice=invoice, price=invoice.price, customer=invoice.customer, gateway=gateway) + + @classmethod + def create(cls, user, cart, address, gateway): + with transaction.atomic(): + invoice = cls.objects.create(customer=user, cart=cart, price=cart.total_price, address=address) + payment = cls.create_payment(invoice=invoice, gateway=gateway) + + return payment + + def __str__(self): + return f"{self.customer} - {self.price} - {'Paid' if self.is_paid else 'Not paid'}" + + +class Payment(BaseModel): + uuid = models.UUIDField(unique=True, verbose_name=_('uuid'), db_index=True, default=uuid.uuid4) + invoice = models.ForeignKey(Invoice, verbose_name=_('invoice'), related_name='payment', on_delete=models.CASCADE) + price = models.IntegerField(verbose_name=_('price')) + customer = models.ForeignKey(Customer, verbose_name=_('customer'), related_name='payments', + on_delete=models.SET_NULL, null=True) + is_paid = models.BooleanField(verbose_name=_('is paid'), default=False) + gateway = models.ForeignKey(Gateway, verbose_name=_('gateway'), related_name='payments', on_delete=models.SET_NULL, + null=True) + payment_log = models.TextField(verbose_name=_('logs'), blank=True) + authority = models.CharField(max_length=64, verbose_name=_('authority'), blank=True) + + class Meta: + verbose_name = _('Payment') + verbose_name_plural = _('Payments') + db_table = 'payment' + + def __str__(self): + return f"{self.price} - {'Paid' if self.is_paid else 'Not paid'}" + + def get_request_handler_data(self): + return dict( + amount=self.price, description=self.description, user_email=getattr(self.customer, 'email', None), + user_phone_number=self.customer.phone_number, REQUEST_URL=self.gateway.gateway_request_url, + MERCHANT_ID=self.gateway.auth_data, CALL_BACK=CALL_BACK, + ) + + @property + def bank_page(self): + handler = self.gateway.get_request_handler() + if handler: + link, authority = handler(**self.get_request_handler_data()) + if authority: + self.authority = authority + self.save() + return link + + def get_verify_handler_data(self): + return dict( + amount=self.price, authority=self.authority, REQUEST_URL=self.gateway.gateway_request_url, + MERCHANT_ID=self.gateway.auth_data + ) + + def verify(self): + from order.models import Order + handler = self.gateway.get_verify_handler() + if handler: + is_paid, ref_id = handler(**self.get_verify_handler_data()) + if is_paid: + with transaction.atomic(): + self.is_paid = True + self.invoice.is_paid = True + self.invoice.cart.is_paid = True + self.invoice.cart.save() + self.invoice.save() + self.save() + Order.create(invoice=self.invoice) + return is_paid, ref_id + + @property + def description(self): + return 'buy of snapp food service' diff --git a/payment/templates/payment/checkout.html b/payment/templates/payment/checkout.html new file mode 100644 index 0000000..b4c43cb --- /dev/null +++ b/payment/templates/payment/checkout.html @@ -0,0 +1,9 @@ +{% extends 'base.html' %} +{% block content %} +
+ {% csrf_token %} + {{ address_form.as_p }} + {{ gateway_form.as_p }} + +
+{% endblock %} diff --git a/payment/templates/payment/verify.html b/payment/templates/payment/verify.html new file mode 100644 index 0000000..b96b425 --- /dev/null +++ b/payment/templates/payment/verify.html @@ -0,0 +1,17 @@ +{% extends 'base.html' %} +{% block title %}Payment Verification{% endblock %} +{% block content %} + {% if is_paid %} + +
+

Payment was successful

+
Your ref code: {{ ref_id }}
+
+ {% else %} +
+

Payment was successful

+
Your ref code: {{ ref_id }}
+
+ {% endif %} + +{% endblock %} \ No newline at end of file diff --git a/payment/tests.py b/payment/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/payment/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/payment/urls.py b/payment/urls.py new file mode 100644 index 0000000..e9d8f62 --- /dev/null +++ b/payment/urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from payment.views import CheckoutView, PaymentVerify + +app_name = 'payment' + +urlpatterns = [ + path('checkout/', CheckoutView.as_view(), name='checkout'), + path('verify/', PaymentVerify.as_view(), name='verify'), +] diff --git a/payment/views.py b/payment/views.py new file mode 100644 index 0000000..619e65c --- /dev/null +++ b/payment/views.py @@ -0,0 +1,68 @@ +from django.contrib.auth.decorators import login_required +from django.db import transaction +from django.http import HttpResponse, HttpResponseRedirect +from django.shortcuts import render, get_object_or_404 +from django.urls import reverse_lazy +from django.utils.decorators import method_decorator +from django.views import View +from django.views.decorators.http import require_http_methods + +from address.models import CustomerAddress +from cart.models import Cart +from library.utils import CustomUserPasses +from payment.forms import AddressSelectForm, GatewaySelectForm +from payment.models import Payment, Invoice + + +@method_decorator(require_http_methods(['POST', 'GET']), name='dispatch') +@method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch') +class CheckoutView(CustomUserPasses, View): + + def test_func(self): + if not self.cart.lines.exists(): + return False + return True + + def dispatch(self, request, *args, **kwargs): + cart_id = self.request.COOKIES.get('cart_id', None) + self.cart = get_object_or_404(Cart, pk=cart_id) + if self.cart.customer is None: + self.cart.customer = request.user + self.cart.save() + + return super().dispatch(request, *args, **kwargs) + + def get(self, request, *args, **kwargs): + address_form = AddressSelectForm() + address_form.base_fields['address'].queyset = CustomerAddress.objects.filter(customer_user=request.user) + gateway_form = GatewaySelectForm() + return render(request, 'payment/checkout.html', + context={'address_form': address_form, 'gateway_form': gateway_form} + ) + + def post(self, request, *args, **kwargs): + address_form = AddressSelectForm(request.POST) + gateway_form = GatewaySelectForm(request.POST) + + if address_form.is_valid() and gateway_form.is_valid(): + address = address_form.cleaned_data['address'] + gateway = gateway_form.cleaned_data['gateway'] + with transaction.atomic(): + payment = Invoice.create(request.user, self.cart, address=address, gateway=gateway) + bank_url = payment.bank_page + + return HttpResponseRedirect(bank_url) + return self.get(request, *args, **kwargs) + + +@method_decorator(require_http_methods(['GET']), name='dispatch') +@method_decorator(login_required(login_url=reverse_lazy('accounts:customer-login-register')), name='dispatch') +class PaymentVerify(View): + def get(self, request, *args, **kwargs): + authority = self.request.GET.get('Authority', None) + if authority: + payment = get_object_or_404(Payment, authority=authority) + is_paid, ref_id = payment.verify() + response = render(request, 'payment/verify.html', {'is_paid': is_paid, 'ref_id': ref_id}) + response.delete_cookie('cart_id') + return response diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..96437c1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +asgiref==3.4.1 +Django==3.2 +django-filter==2.4.0 +Pillow==8.3.1 +psycopg2-binary==2.9.1 +pytz==2021.1 +sqlparse==0.4.1 +suds-jurko==0.6 diff --git a/service/__init__.py b/service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service/admin.py b/service/admin.py new file mode 100644 index 0000000..bd061c4 --- /dev/null +++ b/service/admin.py @@ -0,0 +1,30 @@ +from django.contrib import admin +from .models import Service, ServiceCategory, DeliveryArea, ServiceAvailableTime + + +@admin.register(Service) +class ServiceAdmin(admin.ModelAdmin): + list_display = ('uuid', 'name', 'service_type', 'minimum_purchase', 'address') + list_filter = ('service_type',) + search_fields = ('name',) + + +@admin.register(ServiceCategory) +class ServiceCategoryAdmin(admin.ModelAdmin): + list_display = ('name', 'slug', 'service') + search_fields = ('name', 'service') + + +@admin.register(DeliveryArea) +class DeliveryAreaAdmin(admin.ModelAdmin): + list_display = ('service', 'area') + search_fields = ('service', 'area') + + +@admin.register(ServiceAvailableTime) +class ServiceAvailableTimeAdmin(admin.ModelAdmin): + list_display = ('service', 'day', 'open_time', 'close_time', 'is_close') + list_filter = ('day', 'is_close') + search_fields = ('service',) + + diff --git a/service/apps.py b/service/apps.py new file mode 100644 index 0000000..8d0ae67 --- /dev/null +++ b/service/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ServiceConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'service' diff --git a/service/filters.py b/service/filters.py new file mode 100644 index 0000000..41920a9 --- /dev/null +++ b/service/filters.py @@ -0,0 +1,16 @@ +from django import forms + +import django_filters + +from service.models import Service + + +class ServiceFilter(django_filters.FilterSet): + name = django_filters.CharFilter( + widget=forms.TextInput(attrs={'class': 'form-control'}), lookup_expr='icontains' + ) + + service_type = django_filters.ChoiceFilter( + choices=Service.SERVICE_TYPES, + widget=forms.Select(attrs={'class': 'form-control'}) + ) diff --git a/service/forms.py b/service/forms.py new file mode 100644 index 0000000..7b55c5e --- /dev/null +++ b/service/forms.py @@ -0,0 +1,43 @@ +from django import forms + +from service.models import Service, ServiceCategory, DeliveryArea, ServiceAvailableTime + + +class ServiceCreateUpdateForm(forms.ModelForm): + class Meta: + model = Service + fields = ('name', 'service_type', 'minimum_purchase', 'available', 'logo', 'banner') + widgets = { + 'name': forms.TextInput(attrs={'class': 'form-control'}), + 'service_type': forms.Select(attrs={'class': 'form-control'}), + 'minimum_purchase': forms.NumberInput(attrs={'class': 'form-control'}), + 'logo': forms.FileInput(attrs={'class': 'form-control'}), + 'banner': forms.FileInput(attrs={'class': 'form-control'}), + 'available': forms.NullBooleanSelect(attrs={'class': 'form-control'}), + } + + +class ServiceCategoryCreateUpdateForm(forms.ModelForm): + class Meta: + model = ServiceCategory + fields = ('name',) + widgets = {'name': forms.TextInput(attrs={'class': 'form-control'})} + + +class DeliveryAreaCreateUpdateForm(forms.ModelForm): + class Meta: + model = DeliveryArea + fields = ('area',) + widgets = {'area': forms.Select(attrs={'class': 'form-control'})} + + +class ServiceAvailableTimeCreateUpdateForm(forms.ModelForm): + class Meta: + model = ServiceAvailableTime + fields = ('day', 'open_time', 'close_time', 'is_close') + widgets = { + 'day': forms.Select(attrs={'class': 'form-control'}), + 'open_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}), + 'close_time': forms.TimeInput(attrs={'type': 'time', 'class': 'form-control'}), + 'is_close': forms.NullBooleanSelect(attrs={'class': 'form-control'}) + } diff --git a/service/migrations/0001_initial.py b/service/migrations/0001_initial.py new file mode 100644 index 0000000..307ff2b --- /dev/null +++ b/service/migrations/0001_initial.py @@ -0,0 +1,100 @@ +# Generated by Django 3.2 on 2021-08-28 20:31 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('accounts', '0001_initial'), + ('address', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='AvailableTime', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), + ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), + ('day', models.PositiveSmallIntegerField(choices=[(0, 'saturday'), (1, 'sunday'), (2, 'monday'), (3, 'tuesday'), (4, 'wednesday'), (5, 'thursday'), (6, 'friday')], verbose_name='day')), + ('open_time', models.DateTimeField(verbose_name='open time')), + ('close_time', models.DateTimeField(verbose_name='close time')), + ('close_day', models.BooleanField(blank=True, null=True, verbose_name='close day')), + ], + options={ + 'verbose_name': 'AvailableTime', + 'verbose_name_plural': 'AvailableTimes', + 'db_table': 'available_time', + }, + ), + migrations.CreateModel( + name='Service', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), + ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), + ('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True, verbose_name='uuid')), + ('name', models.CharField(max_length=40, verbose_name='name')), + ('service_type', models.PositiveSmallIntegerField(choices=[(0, 'restaurant'), (1, 'cafe'), (2, 'confectionery'), (3, 'supermarket')], verbose_name='service type')), + ('minimum_purchase', models.DecimalField(decimal_places=0, max_digits=9, verbose_name='minimum purchase')), + ('address', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='services', to='address.address', verbose_name='address')), + ('service_provider', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='services', to='accounts.serviceprovider', verbose_name='service provider')), + ], + options={ + 'verbose_name': 'Service', + 'verbose_name_plural': 'Services', + 'db_table': 'service', + }, + ), + migrations.CreateModel( + name='ServiceCategory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), + ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), + ('name', models.CharField(max_length=30, verbose_name='name')), + ('slug', models.SlugField(allow_unicode=True, max_length=35, verbose_name='slug')), + ('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='categories', to='service.service', verbose_name='service')), + ], + options={ + 'verbose_name': 'ServiceCategory', + 'verbose_name_plural': 'ServiceCategories', + 'db_table': 'service_category', + }, + ), + migrations.CreateModel( + name='ServiceAvailableTime', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), + ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), + ('available_time', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='service.availabletime', verbose_name='available time')), + ('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='available_times', to='service.service', verbose_name='service')), + ], + options={ + 'verbose_name': 'ServiceAvailableTime', + 'verbose_name_plural': 'ServiceAvailableTimes', + 'db_table': 'service_available_time', + }, + ), + migrations.CreateModel( + name='DeliveryArea', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='created time')), + ('modified_time', models.DateTimeField(auto_now=True, verbose_name='modified time')), + ('area', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='delivery_areas', to='address.area', verbose_name='area')), + ('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='delivery_areas', to='service.service', verbose_name='service')), + ], + options={ + 'verbose_name': 'DeliveryArea', + 'verbose_name_plural': 'DeliveryAreas', + 'db_table': 'delivery_area', + }, + ), + ] diff --git a/service/migrations/0002_alter_service_address.py b/service/migrations/0002_alter_service_address.py new file mode 100644 index 0000000..d26148c --- /dev/null +++ b/service/migrations/0002_alter_service_address.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2 on 2021-08-30 08:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('address', '0002_auto_20210830_1305'), + ('service', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='service', + name='address', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='services', to='address.serviceaddress', verbose_name='address'), + ), + ] diff --git a/service/migrations/0003_alter_service_address.py b/service/migrations/0003_alter_service_address.py new file mode 100644 index 0000000..972ed47 --- /dev/null +++ b/service/migrations/0003_alter_service_address.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2 on 2021-08-31 11:56 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('address', '0006_rename_user_customeraddress_customer_user'), + ('service', '0002_alter_service_address'), + ] + + operations = [ + migrations.AlterField( + model_name='service', + name='address', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='services', to='address.serviceaddress', verbose_name='address'), + ), + ] diff --git a/service/migrations/0004_alter_service_address.py b/service/migrations/0004_alter_service_address.py new file mode 100644 index 0000000..04a0d58 --- /dev/null +++ b/service/migrations/0004_alter_service_address.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2 on 2021-08-31 11:58 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('address', '0006_rename_user_customeraddress_customer_user'), + ('service', '0003_alter_service_address'), + ] + + operations = [ + migrations.AlterField( + model_name='service', + name='address', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='services', to='address.serviceaddress', verbose_name='address'), + ), + ] diff --git a/service/migrations/0005_auto_20210902_1522.py b/service/migrations/0005_auto_20210902_1522.py new file mode 100644 index 0000000..61db266 --- /dev/null +++ b/service/migrations/0005_auto_20210902_1522.py @@ -0,0 +1,40 @@ +# Generated by Django 3.2 on 2021-09-02 10:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('service', '0004_alter_service_address'), + ] + + operations = [ + migrations.RemoveField( + model_name='serviceavailabletime', + name='available_time', + ), + migrations.AddField( + model_name='serviceavailabletime', + name='close_time', + field=models.TimeField(null=True, verbose_name='close time'), + ), + migrations.AddField( + model_name='serviceavailabletime', + name='day', + field=models.PositiveSmallIntegerField(choices=[(0, 'saturday'), (1, 'sunday'), (2, 'monday'), (3, 'tuesday'), (4, 'wednesday'), (5, 'thursday'), (6, 'friday')], null=True, verbose_name='day'), + ), + migrations.AddField( + model_name='serviceavailabletime', + name='is_close', + field=models.BooleanField(blank=True, null=True, verbose_name='close day'), + ), + migrations.AddField( + model_name='serviceavailabletime', + name='open_time', + field=models.TimeField(null=True, verbose_name='open time'), + ), + migrations.DeleteModel( + name='AvailableTime', + ), + ] diff --git a/service/migrations/0006_auto_20210902_1943.py b/service/migrations/0006_auto_20210902_1943.py new file mode 100644 index 0000000..c037678 --- /dev/null +++ b/service/migrations/0006_auto_20210902_1943.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2 on 2021-09-02 15:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('service', '0005_auto_20210902_1522'), + ] + + operations = [ + migrations.AlterModelOptions( + name='serviceavailabletime', + options={'ordering': ('day',), 'verbose_name': 'ServiceAvailableTime', 'verbose_name_plural': 'ServiceAvailableTimes'}, + ), + migrations.AddField( + model_name='service', + name='banner', + field=models.ImageField(blank=True, null=True, upload_to='service/banner', verbose_name='banner'), + ), + migrations.AddField( + model_name='service', + name='logo', + field=models.ImageField(blank=True, null=True, upload_to='service/logo', verbose_name='logo'), + ), + ] diff --git a/service/migrations/0007_auto_20210902_2056.py b/service/migrations/0007_auto_20210902_2056.py new file mode 100644 index 0000000..d32e53b --- /dev/null +++ b/service/migrations/0007_auto_20210902_2056.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2 on 2021-09-02 16:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('service', '0006_auto_20210902_1943'), + ] + + operations = [ + migrations.AlterModelOptions( + name='service', + options={'ordering': ('created_time',), 'verbose_name': 'Service', 'verbose_name_plural': 'Services'}, + ), + migrations.AlterField( + model_name='service', + name='banner', + field=models.ImageField(blank=True, null=True, upload_to='service/banners/', verbose_name='banner'), + ), + migrations.AlterField( + model_name='service', + name='logo', + field=models.ImageField(blank=True, null=True, upload_to='service/logos/', verbose_name='logo'), + ), + ] diff --git a/service/migrations/0008_service_available.py b/service/migrations/0008_service_available.py new file mode 100644 index 0000000..f3c5efa --- /dev/null +++ b/service/migrations/0008_service_available.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2 on 2021-09-02 16:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('service', '0007_auto_20210902_2056'), + ] + + operations = [ + migrations.AddField( + model_name='service', + name='available', + field=models.BooleanField(default=False, verbose_name='available'), + ), + ] diff --git a/service/migrations/__init__.py b/service/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service/models.py b/service/models.py new file mode 100644 index 0000000..9d8da92 --- /dev/null +++ b/service/models.py @@ -0,0 +1,124 @@ +from django.db import models +from django.utils.text import slugify + +from accounts.models import ServiceProvider +from library.models import BaseModel +from django.utils.translation import ugettext_lazy as _ +from address.models import ServiceAddress, Area +import uuid + + +class Service(BaseModel): + RESTAURANT = 0 + CAFE = 1 + CONFECTIONERY = 2 + SUPERMARKET = 3 + + SERVICE_TYPES = ( + (RESTAURANT, _('restaurant')), + (CAFE, _('cafe')), + (CONFECTIONERY, _('confectionery')), + (SUPERMARKET, _('supermarket')), + ) + + uuid = models.UUIDField(default=uuid.uuid4, verbose_name=_('uuid'), unique=True, db_index=True) + service_provider = models.ForeignKey( + ServiceProvider, + verbose_name=_('service provider'), + related_name='services', + on_delete=models.CASCADE + ) + name = models.CharField(max_length=40, verbose_name=_('name')) + service_type = models.PositiveSmallIntegerField(verbose_name=_('service type'), choices=SERVICE_TYPES) + minimum_purchase = models.DecimalField(max_digits=9, decimal_places=0, verbose_name=_('minimum purchase')) + available = models.BooleanField(default=False, verbose_name=_('available')) + logo = models.ImageField(verbose_name=_('logo'), upload_to='service/logos/', null=True, blank=True) + banner = models.ImageField(verbose_name=_('banner'), upload_to='service/banners/', null=True, blank=True) + + address = models.OneToOneField( + ServiceAddress, + verbose_name=_('address'), + related_name='services', + on_delete=models.SET_NULL, + null=True, + blank=True + ) + + def __str__(self): + return f'{self.name} - {self.get_service_type_display()}' + + class Meta: + verbose_name = _('Service') + verbose_name_plural = _('Services') + db_table = 'service' + ordering = ('created_time',) + + +class ServiceCategory(BaseModel): + name = models.CharField(max_length=30, verbose_name=_('name')) + slug = models.SlugField(max_length=35, verbose_name=_('slug'), allow_unicode=True) + service = models.ForeignKey(Service, verbose_name=_('service'), related_name='categories', on_delete=models.CASCADE) + + def __str__(self): + return f'{self.name} - {self.service.name}' + + def save(self, *args, **kwargs): + self.slug = slugify(self.name, allow_unicode=True) + super().save(*args, **kwargs) + + class Meta: + verbose_name = _('ServiceCategory') + verbose_name_plural = _('ServiceCategories') + db_table = 'service_category' + + +class DeliveryArea(BaseModel): + service = models.ForeignKey( + Service, verbose_name=_('service'), related_name='delivery_areas', on_delete=models.CASCADE + ) + area = models.ForeignKey(Area, verbose_name=_('area'), related_name='delivery_areas', on_delete=models.CASCADE) + + def __str__(self): + return f'{self.service.name} - {self.area}' + + class Meta: + verbose_name = _('DeliveryArea') + verbose_name_plural = _('DeliveryAreas') + db_table = 'delivery_area' + + +class ServiceAvailableTime(BaseModel): + SAT_DAY = 0 + SUN_DAY = 1 + MON_DAY = 2 + TUE_DAY = 3 + WED_DAY = 4 + THU_DAY = 5 + FRI_DAY = 6 + + DAYS = ( + (SAT_DAY, _('saturday')), + (SUN_DAY, _('sunday')), + (MON_DAY, _('monday')), + (TUE_DAY, _('tuesday')), + (WED_DAY, _('wednesday')), + (THU_DAY, _('thursday')), + (FRI_DAY, _('friday')) + ) + service = models.ForeignKey( + Service, verbose_name=_('service'), related_name='available_times', on_delete=models.CASCADE + ) + day = models.PositiveSmallIntegerField(verbose_name=_('day'), choices=DAYS, null=True) + open_time = models.TimeField(verbose_name=_('open time'), null=True) + close_time = models.TimeField(verbose_name=_('close time'), null=True) + is_close = models.BooleanField(verbose_name=_('close day'), blank=True, null=True) + + def __str__(self): + return f'{self.service.name} - {self.get_day_display()} - ' \ + f'{self.is_close if self.close_time else self.open_time and "-" and self.close_time}' + + class Meta: + verbose_name = _('ServiceAvailableTime') + verbose_name_plural = _('ServiceAvailableTimes') + db_table = 'service_available_time' + ordering = ('day',) diff --git a/service/templates/delivery_area/create_update_form.html b/service/templates/delivery_area/create_update_form.html new file mode 100644 index 0000000..3406e70 --- /dev/null +++ b/service/templates/delivery_area/create_update_form.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% block content %} +
+ {% csrf_token %} + {{ form.as_p }} + +
+ {% if object %} + cancel + {% else %} + cancel + {% endif %} +{% endblock %} diff --git a/service/templates/delivery_area/delete_form.html b/service/templates/delivery_area/delete_form.html new file mode 100644 index 0000000..37b3357 --- /dev/null +++ b/service/templates/delivery_area/delete_form.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} +{% block content %} +
+
+ {% csrf_token %} +

Are you sure to delete service category ({{ delivery.area.name }})?

+ +
+ cancel +
+{% endblock %} diff --git a/service/templates/service/list.html b/service/templates/service/list.html new file mode 100644 index 0000000..70013cd --- /dev/null +++ b/service/templates/service/list.html @@ -0,0 +1,61 @@ +{% extends 'base.html' %} +{% load static %} +{% load service_delivary_area %} +{% block content %} +
+
+
+
+
+ {{ filter.form.as_p }} + +
+
+
+
+
+
+ {% for service in services %} + +
+
+
+
+ {% if service.banner %} + + {% else %} + + {% endif %} +
+
+ {% if service.logo %} + + {% else %} + + {% endif %} +
+
+
+
{{ service.name.title }}
+
+

{{ service.address | truncatechars:30 }}

+ {% delivery_area_string service as areas %} +

{{ areas | truncatechars:15 }}

+

{{ service.areas | truncatechars:20 }}

+ +
+ +
+ Menu +
+
+
+
+
+ {% endfor %} +
+
+
+{% endblock %} \ No newline at end of file diff --git a/service/templates/service/service_provider/create_update_form.html b/service/templates/service/service_provider/create_update_form.html new file mode 100644 index 0000000..4ab3de7 --- /dev/null +++ b/service/templates/service/service_provider/create_update_form.html @@ -0,0 +1,9 @@ +{% extends 'base.html' %} +{% block content %} +
+ {% csrf_token %} + {{ form.as_p }} + +
+ cancel +{% endblock %} diff --git a/service/templates/service/service_provider/delete_form.html b/service/templates/service/service_provider/delete_form.html new file mode 100644 index 0000000..bd2fc5b --- /dev/null +++ b/service/templates/service/service_provider/delete_form.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% block content %} +
+
+ {% csrf_token %} +

Are you sure to delete service ({{ service.name }})?

+ +
+ cancel +
+{% endblock %} diff --git a/service/templates/service/service_provider/detail.html b/service/templates/service/service_provider/detail.html new file mode 100644 index 0000000..d49d904 --- /dev/null +++ b/service/templates/service/service_provider/detail.html @@ -0,0 +1,119 @@ +{% extends 'base.html' %} +{% block content %} +
+

Service Detail

+

Service name : {{ service.name }}

+

Service minimum purchase : {{ service.minimum_purchase }}

+

Service minimum purchase : {{ service.get_service_type_display }}

+ {% if service.available %} +

Service available : Yes

+ {% else %} +

Service available : No

+ {% endif %} + {% if service.logo %} +

Service Logo Image :

+ {% else %} +

Service Logo Image : Not Yet Uploaded

+ {% endif %} + {% if service.banner %} +

Service Banner Image :

+ {% else %} +

Service Logo Image : Not Yet Uploaded

+ {% endif %} + +
+
+
+

Service Address

+ {% if service.address %} +

state : {{ service.address.state.name }}

+

city : {{ service.address.city.name }}

+

area : {{ service.address.area.name }}

+

street : {{ service.address.street }}

+

alley : {{ service.address.alley }}

+

floor : {{ service.address.floor }}

+

plaque : {{ service.address.plaque }}

+ Update Address + {% else %} +

Your service yet not has address!Create Address

+ {% endif %} +
+
+
+

Service Categories

+

+ Create new category +

+
    + {% for category in service.categories.all %} +
  • + {{ category.name }} +
    + Update + Detail + Delete +
    +
  • + {% endfor %} +
+
+
+
+

Service Delivery Area

+
+
+ Create new delivery area +
+
+ {% with deliveries=service.delivery_areas.all %} + {% if deliveries %} + {% for delivery in deliveries %} +
  • + {{ delivery.area.name }} +
    + Update + Delete +
    +
  • + {% endfor %} + + {% endif %} + {% endwith %} +
    +
    +
    +

    Service Available Time

    +
    +
    + Create new available + time +
    +
    + {% with availabletimes=service.available_times.all %} + {% if availabletimes %} + {% for availabletime in availabletimes %} +
  • + {{ availabletime.get_day_display }} | open at : {{ availabletime.open_time }} | close at + : {{ availabletime.close_time }} | is close : {{ availabletime.is_close }} +
    + Update + Delete +
    +
  • + {% endfor %} + + {% endif %} + {% endwith %} +
    +
    +{% endblock %} diff --git a/service/templates/service/service_provider/list.html b/service/templates/service/service_provider/list.html new file mode 100644 index 0000000..e62ea8e --- /dev/null +++ b/service/templates/service/service_provider/list.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} +{% block content %} + Create new service +

    + +{% endblock %} diff --git a/service/templates/service_available_time/create_update_form.html b/service/templates/service_available_time/create_update_form.html new file mode 100644 index 0000000..3406e70 --- /dev/null +++ b/service/templates/service_available_time/create_update_form.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% block content %} +
    + {% csrf_token %} + {{ form.as_p }} + +
    + {% if object %} + cancel + {% else %} + cancel + {% endif %} +{% endblock %} diff --git a/service/templates/service_available_time/delete_form.html b/service/templates/service_available_time/delete_form.html new file mode 100644 index 0000000..d6e6b9e --- /dev/null +++ b/service/templates/service_available_time/delete_form.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} +{% block content %} +
    +
    + {% csrf_token %} +

    Are you sure to delete service available time ({{ available_time.get_day_display }})?

    + +
    + cancel +
    +{% endblock %} diff --git a/service/templates/service_category/create_update_form.html b/service/templates/service_category/create_update_form.html new file mode 100644 index 0000000..6bcb436 --- /dev/null +++ b/service/templates/service_category/create_update_form.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} +{% block content %} +
    + {% csrf_token %} + {{ form.as_p }} + +
    + {% if service %} + cancel + {% else %} + cancel + {% endif %} +{% endblock %} diff --git a/service/templates/service_category/delete_form.html b/service/templates/service_category/delete_form.html new file mode 100644 index 0000000..19ee37a --- /dev/null +++ b/service/templates/service_category/delete_form.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} +{% block content %} +
    +
    + {% csrf_token %} +

    Are you sure to delete service category ({{ category.name }})?

    + +
    + cancel +
    +{% endblock %} diff --git a/service/templates/service_category/detail.html b/service/templates/service_category/detail.html new file mode 100644 index 0000000..5800e01 --- /dev/null +++ b/service/templates/service_category/detail.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} +{% block content %} +
    +

    Category Detail

    +

    Category name : {{ category.name }}

    +
    +

    Service Items

    + {% if category.items.all %} +
      + {% for item in category.items.all %} +
    • + {{ category.name }} +
      + Update + Delete +
      +
    • + {% endfor %} +
    + {% else %} +

    Your category yet not has item!Create Item

    + {% endif %} +
    +{% endblock %} diff --git a/service/templatetags/__init__.py b/service/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service/templatetags/service_delivary_area.py b/service/templatetags/service_delivary_area.py new file mode 100644 index 0000000..0bd4aa1 --- /dev/null +++ b/service/templatetags/service_delivary_area.py @@ -0,0 +1,11 @@ +from django import template + +register = template.Library() + + +@register.simple_tag() +def delivery_area_string(service): + area_list = list() + for delivery in service.delivery_areas.all(): + area_list.append(delivery.area.name) + return ', '.join(area_list) diff --git a/service/tests.py b/service/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/service/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/service/urls.py b/service/urls.py new file mode 100644 index 0000000..df16dab --- /dev/null +++ b/service/urls.py @@ -0,0 +1,48 @@ +from django.urls import path + +from .views import ServiceProviderServiceCreateView, ServiceProviderServiceUpdateView, ServiceProviderServiceDeleteView, \ + ServiceProviderServiceDetailView, ServiceProviderServiceListView, ServiceProviderServiceCategoryCreateView, \ + ServiceProviderServiceCategoryUpdateView, ServiceProviderServiceCategoryDeleteView, \ + ServiceProviderDeliveryAreaCreate, ServiceProviderDeliveryUpdateView, \ + ServiceProviderDeliveryDeleteView, ServiceProviderServiceAvailableTimeCreateView, \ + ServiceProviderServiceAvailableTimeUpdateView, ServiceProviderServiceAvailableTimeDeleteView, ServiceListView + +app_name = 'service' + +urlpatterns = ( + path('serviceprovider/create/', ServiceProviderServiceCreateView.as_view(), + name='service-provider-service-create'), + path('serviceprovider/update/', ServiceProviderServiceUpdateView.as_view(), + name='service-provider-service-update'), + path('serviceprovider/delete/', ServiceProviderServiceDeleteView.as_view(), + name='service-provider-service-delete'), + path('serviceprovider/detail/', ServiceProviderServiceDetailView.as_view(), + name='service-provider-service-detail'), + path('serviceprovider/list/', ServiceProviderServiceListView.as_view(), name='service-provider-service-list'), + + path('serviceprovider/category/create//', ServiceProviderServiceCategoryCreateView.as_view(), + name='service-provider-service-category-create'), + path('serviceprovider/category/update//', ServiceProviderServiceCategoryUpdateView.as_view(), + name='service-provider-service-category-update'), + path('serviceprovider/category/delete//', ServiceProviderServiceCategoryDeleteView.as_view(), + name='service-provider-service-category-delete'), + + path('serviceprovider/deliveryarea/create/', ServiceProviderDeliveryAreaCreate.as_view(), + name='service-provider-delivery-area-create'), + path('serviceprovider/deliveryarea/update/', ServiceProviderDeliveryUpdateView.as_view(), + name='service-provider-delivery-area-update'), + path('serviceprovider/deliveryarea/delete/', ServiceProviderDeliveryDeleteView.as_view(), + name='service-provider-delivery-area-delete'), + + path( + 'serviceprovider/availabletime/create/', + ServiceProviderServiceAvailableTimeCreateView.as_view(), + name='service-provider-available-time-create' + ), + path('serviceprovider/availabletime/update/', ServiceProviderServiceAvailableTimeUpdateView.as_view(), + name='service-provider-available-time-update'), + path('serviceprovider/availabletime/delete/', ServiceProviderServiceAvailableTimeDeleteView.as_view(), + name='service-provider-available-time-delete'), + + path('list/', ServiceListView.as_view(), name='service-list') +) diff --git a/service/utils.py b/service/utils.py new file mode 100644 index 0000000..edc125b --- /dev/null +++ b/service/utils.py @@ -0,0 +1,10 @@ +from abc import ABC + +from accounts.utils import IsServiceProvider + + +class CustomServiceIsServiceProvider(ABC, IsServiceProvider): + def test_func(self): + result = super().test_func() + obj = self.get_object() + return obj.service_provider == self.request.user and result diff --git a/service/views.py b/service/views.py new file mode 100644 index 0000000..6671536 --- /dev/null +++ b/service/views.py @@ -0,0 +1,189 @@ +from abc import ABC + +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.db import transaction +from django.urls import reverse_lazy +from django.utils.decorators import method_decorator +from django.views.generic import CreateView, ListView, UpdateView, DeleteView, DetailView +from django.shortcuts import get_object_or_404, redirect + +from django_filters.views import FilterView + +from accounts.utils import IsServiceProvider +from service.forms import ServiceCreateUpdateForm, ServiceCategoryCreateUpdateForm, DeliveryAreaCreateUpdateForm, \ + ServiceAvailableTimeCreateUpdateForm +from service.filters import ServiceFilter +from service.models import Service, ServiceCategory, DeliveryArea, ServiceAvailableTime +from service.utils import CustomServiceIsServiceProvider + + +class BaseServiceView(ABC): + model = Service + form_class = ServiceCreateUpdateForm + template_name = 'service/service_provider/create_update_form.html' + + +class BaseCreateView(ABC, IsServiceProvider): + def test_func(self): + return self.service.service_provider == self.request.user and super().test_func() + + def get_success_url(self): + return reverse_lazy('service:service-provider-service-detail', kwargs={'pk': self.service.pk}) + + def setup(self, request, *args, **kwargs): + super().setup(request, *args, **kwargs) + self.service = get_object_or_404(Service, pk=self.kwargs['service_pk']) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['service'] = self.service + return context + + +class BaseOtherView(IsServiceProvider): + def get_success_url(self): + return reverse_lazy('service:service-provider-service-detail', kwargs={'pk': self.object.service.pk}) + + def test_func(self): + obj = self.get_object() + return obj.service.service_provider == self.request.user and super().test_func() + + +class BaseServiceCategory(ABC, BaseOtherView): + model = ServiceCategory + form_class = ServiceCategoryCreateUpdateForm + template_name = 'service_category/create_update_form.html' + + +class BaseDeliveryArea(ABC, BaseOtherView): + model = DeliveryArea + form_class = DeliveryAreaCreateUpdateForm + template_name = 'delivery_area/create_update_form.html' + + +class BaseServiceAvailableTime(ABC, BaseOtherView): + model = ServiceAvailableTime + form_class = ServiceAvailableTimeCreateUpdateForm + template_name = 'service_available_time/create_update_form.html' + + +@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') +class ServiceProviderServiceCreateView(BaseServiceView, IsServiceProvider, CreateView): + success_url = reverse_lazy('service:service-provider-service-list') + + def form_valid(self, form): + instance = form.save(commit=False) + instance.service_provider = self.request.user + instance.save() + return super().form_valid(form) + + +@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') +class ServiceProviderServiceUpdateView(BaseServiceView, CustomServiceIsServiceProvider, UpdateView): + success_url = reverse_lazy('service:service-provider-service-list') + + +@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') +class ServiceProviderServiceDeleteView(BaseServiceView, CustomServiceIsServiceProvider, DeleteView): + template_name = 'service/service_provider/delete_form.html' + success_url = reverse_lazy('service:service-provider-service-list') + + +@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') +class ServiceProviderServiceDetailView(BaseServiceView, CustomServiceIsServiceProvider, DetailView): + context_object_name = 'service' + template_name = 'service/service_provider/detail.html' + + +@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') +class ServiceProviderServiceListView(BaseServiceView, IsServiceProvider, ListView): + context_object_name = 'services' + template_name = 'service/service_provider/list.html' + + def get_queryset(self): + return super().get_queryset().filter(service_provider=self.request.user) + + +@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') +class ServiceProviderServiceCategoryCreateView(BaseCreateView, CreateView): + model = ServiceCategory + form_class = ServiceCategoryCreateUpdateForm + template_name = 'service_category/create_update_form.html' + + def form_valid(self, form): + instance = form.save(commit=False) + instance.service = self.service + instance.save() + return super().form_valid(form) + + +@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') +class ServiceProviderServiceCategoryUpdateView(BaseServiceCategory, UpdateView): + pass + + +@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') +class ServiceProviderServiceCategoryDeleteView(BaseServiceCategory, DeleteView): + context_object_name = 'category' + template_name = 'service_category/delete_form.html' + + +@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') +class ServiceProviderDeliveryAreaCreate(BaseCreateView, CreateView): + model = DeliveryArea + form_class = DeliveryAreaCreateUpdateForm + template_name = 'delivery_area/create_update_form.html' + + def form_valid(self, form): + instance = form.save(commit=False) + instance.service = self.service + instance.save() + return super().form_valid(form) + + +@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') +class ServiceProviderDeliveryUpdateView(BaseDeliveryArea, UpdateView): + pass + + +@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') +class ServiceProviderDeliveryDeleteView(BaseDeliveryArea, DeleteView): + context_object_name = 'delivery' + template_name = 'delivery_area/delete_form.html' + + +@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') +class ServiceProviderServiceAvailableTimeCreateView(BaseCreateView, CreateView): + model = ServiceAvailableTime + form_class = ServiceAvailableTimeCreateUpdateForm + template_name = 'service_available_time/create_update_form.html' + + def form_valid(self, form): + with transaction.atomic(): + instance = form.save(commit=False) + if self.service.available_times.filter(day=instance.day).exists(): + messages.info(self.request, f'This Service {self.service.name} has this day time', 'danger') + return redirect(self.get_success_url()) + instance.service = self.service + instance.save() + return super().form_valid(form) + + +@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') +class ServiceProviderServiceAvailableTimeUpdateView(BaseServiceAvailableTime, UpdateView): + pass + + +@method_decorator(login_required(login_url=reverse_lazy('accounts:service-provider-login')), name='dispatch') +class ServiceProviderServiceAvailableTimeDeleteView(BaseServiceAvailableTime, DeleteView): + context_object_name = 'available_time' + template_name = 'service_available_time/delete_form.html' + + +class ServiceListView(FilterView): + model = Service + context_object_name = 'services' + template_name = 'service/list.html' + filterset_class = ServiceFilter + queryset = Service.objects.filter(available=True) diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..f2ad898 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,48 @@ +{% load accounts_tags %} + + + + + + + + + + {% block title %} {% endblock %} + {% block extra_css %}{% endblock %} + + + +{% is_customer request.user as customer_flag %} +{% is_service_provider request.user as service_provider_flag %} +{% is_admin_user request.user as admin_flag %} + + +{% if service_provider_flag %} + {% include 'inc/service_provider_navbar.html' %} +{% elif customer_flag %} + {% include 'inc/customer_navbar.html' %} +{% elif admin_flag %} + {% include 'inc/admin_navbar.html' %} +{% else %} + {% include 'inc/customer_navbar.html' %} +{% endif %} +{% block banner %}{% endblock %} + + +
    + {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + {% block content %} + {% endblock %} +
    + + + + \ No newline at end of file diff --git a/templates/inc/admin_navbar.html b/templates/inc/admin_navbar.html new file mode 100644 index 0000000..c27e362 --- /dev/null +++ b/templates/inc/admin_navbar.html @@ -0,0 +1,42 @@ + \ No newline at end of file diff --git a/templates/inc/customer_navbar.html b/templates/inc/customer_navbar.html new file mode 100644 index 0000000..7532d8a --- /dev/null +++ b/templates/inc/customer_navbar.html @@ -0,0 +1,35 @@ + \ No newline at end of file diff --git a/templates/inc/service_provider_navbar.html b/templates/inc/service_provider_navbar.html new file mode 100644 index 0000000..9b0dc6c --- /dev/null +++ b/templates/inc/service_provider_navbar.html @@ -0,0 +1,48 @@ + \ No newline at end of file diff --git a/yummy/__init__.py b/yummy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yummy/asgi.py b/yummy/asgi.py new file mode 100644 index 0000000..8f47278 --- /dev/null +++ b/yummy/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for yummy project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'yummy.settings') + +application = get_asgi_application() diff --git a/yummy/settings.py b/yummy/settings.py new file mode 100644 index 0000000..9623366 --- /dev/null +++ b/yummy/settings.py @@ -0,0 +1,141 @@ +""" +Django settings for yummy project. + +Generated by 'django-admin startproject' using Django 3.2. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/3.2/ref/settings/ +""" + +import os +from pathlib import Path +from yummy.local_settings import * + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + + 'django_filters', + + 'service.apps.ServiceConfig', + 'order.apps.OrderConfig', + 'address.apps.AddressConfig', + 'accounts.apps.AccountsConfig', + 'item.apps.ItemConfig', + 'cart.apps.CartConfig', + 'payment.apps.PaymentConfig', + 'gateway.apps.GatewayConfig', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'yummy.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'templates/')], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'yummy.wsgi.application' + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': DB_NAME, + 'USER': DB_USER, + 'PASSWORD': DB_PASSWORD, + 'HOST': DB_HOST, + 'PORT': DB_PORT, + } +} + +# Password validation +# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +# Internationalization +# https://docs.djangoproject.com/en/3.2/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'Asia/Tehran' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = '/static/' +STATIC_ROOT = os.path.join(BASE_DIR, 'static') + +MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') + +# Default primary key field type +# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +AUTHENTICATION_BACKENDS = [ + 'django.contrib.auth.backends.ModelBackend', + 'accounts.authenticate.PhoneNumberPasswordBackend', + 'accounts.authenticate.ServiceProviderAuthentication', +] + +CALL_BACK = 'http://127.0.0.1:8000/payment/verify/' diff --git a/yummy/urls.py b/yummy/urls.py new file mode 100644 index 0000000..24fddd1 --- /dev/null +++ b/yummy/urls.py @@ -0,0 +1,37 @@ +"""yummy URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.conf import settings +from django.conf.urls.static import static +from django.contrib import admin +from django.urls import path, include +from django.views.generic import TemplateView + +urlpatterns = [ + path('admin/', admin.site.urls, name='admin-panel'), + path('home/', TemplateView.as_view(template_name='base.html'), name='home'), + path('', TemplateView.as_view(template_name='base.html'), name='home'), + path('accounts/', include('accounts.urls', namespace='accounts')), + path('service/', include('service.urls', namespace='service')), + path('address/', include('address.urls', namespace='address')), + path('item/', include('item.urls', namespace='item')), + path('cart/', include('cart.urls', namespace='cart')), + path('payment/', include('payment.urls', namespace='payment')), + path('order/', include('order.urls', namespace='order')), + +] + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/yummy/wsgi.py b/yummy/wsgi.py new file mode 100644 index 0000000..c07aee4 --- /dev/null +++ b/yummy/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for yummy project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'yummy.settings') + +application = get_wsgi_application()