├── apps ├── __init__.py ├── main │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── tests.py │ ├── views.py │ ├── apps.py │ ├── admin.py │ └── models.py ├── utils │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── views.py │ ├── tests.py │ ├── apps.py │ ├── admin.py │ ├── choices.py │ ├── colors.py │ ├── errors.py │ ├── managers.py │ ├── exceptions.py │ ├── redis.py │ ├── shortcuts.py │ ├── email.py │ ├── permissions.py │ ├── forms.py │ ├── sms.py │ ├── viewsets.py │ └── models.py └── accounts │ ├── __init__.py │ ├── migrations │ ├── __init__.py │ └── 0001_initial.py │ ├── tests.py │ ├── views.py │ ├── routing.py │ ├── apps.py │ ├── urls.py │ ├── forms.py │ ├── models.py │ ├── consumers.py │ ├── admin.py │ ├── serializers.py │ └── viewsets.py ├── run_create_user.sh ├── run_collect_static.sh ├── run_migrate.sh ├── .gitignore ├── project ├── __init__.py ├── celery.py ├── wsgi.py ├── asgi.py ├── urls.py └── settings.py ├── run_theme.sh ├── templates └── errors │ ├── 400.html │ ├── 403.html │ ├── 500.html │ └── 404.html ├── setup ├── nginx │ ├── api │ │ └── default.conf │ └── full │ │ └── default.conf └── themes │ ├── bt.json │ ├── dj.json │ ├── start.json │ ├── usw.json │ └── fd.json ├── manage.py ├── env_template ├── Dockerfile ├── requirements.txt ├── docker-compose.yml └── README.MD /apps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/main/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/accounts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/main/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/utils/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/accounts/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/utils/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | -------------------------------------------------------------------------------- /run_create_user.sh: -------------------------------------------------------------------------------- 1 | docker-compose exec backend python3 manage.py createsuperuser -------------------------------------------------------------------------------- /apps/main/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /apps/main/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /apps/utils/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /apps/accounts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /apps/accounts/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /run_collect_static.sh: -------------------------------------------------------------------------------- 1 | docker exec -ti django_osw4l_full python3 manage.py collectstatic --noinput -------------------------------------------------------------------------------- /run_migrate.sh: -------------------------------------------------------------------------------- 1 | docker-compose exec backend python3 manage.py makemigrations && docker-compose exec backend python3 manage.py migrate -------------------------------------------------------------------------------- /apps/main/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MainConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'apps.main' 7 | -------------------------------------------------------------------------------- /apps/utils/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UtilsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'apps.utils' 7 | -------------------------------------------------------------------------------- /apps/accounts/routing.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | from . import consumers 3 | 4 | auth2_websocket_urlpatterns = [ 5 | re_path(r'ws/users/', consumers.UserStatusConsumer.as_asgi()), 6 | ] -------------------------------------------------------------------------------- /apps/utils/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | # Register your models here. 3 | 4 | admin.site.site_header = 'Osw4l Admin' 5 | admin.site.site_title = 'Osw4l Admin' 6 | admin.site.index_title = 'Osw4l' 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | db.sqlite3 3 | __pycache__/ 4 | *.pyc 5 | *.pyo 6 | celerybeat-schedule.db 7 | celerybeat.pid 8 | /.vscode 9 | /env 10 | /venv 11 | .DS_Store 12 | .env 13 | /app 14 | /setup/docker 15 | /static -------------------------------------------------------------------------------- /apps/accounts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'apps.accounts' 7 | verbose_name = 'accounts' 8 | -------------------------------------------------------------------------------- /project/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | # This will make sure the app is always imported when 4 | # Django starts so that shared_task will use this app. 5 | from .celery import app as celery_app 6 | __all__ = ['celery_app'] 7 | -------------------------------------------------------------------------------- /project/celery.py: -------------------------------------------------------------------------------- 1 | import os 2 | from celery import Celery 3 | from django.conf import settings 4 | 5 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') 6 | 7 | app = Celery('project') 8 | app.config_from_object('django.conf:settings', namespace='CELERY') 9 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) 10 | -------------------------------------------------------------------------------- /apps/utils/choices.py: -------------------------------------------------------------------------------- 1 | OWNER = 'owner' 2 | MANAGER = 'manager' 3 | HUMAN_RESOURCES = 'hr' 4 | ACCOUNTING = 'accounting' 5 | CUSTOMER = 'customer' 6 | 7 | ROLES = ( 8 | (ACCOUNTING, ACCOUNTING.title()), 9 | (OWNER, OWNER.title()), 10 | (MANAGER, MANAGER.title()), 11 | (HUMAN_RESOURCES, 'Human Resources'), 12 | (CUSTOMER, CUSTOMER.title()), 13 | ) 14 | -------------------------------------------------------------------------------- /apps/accounts/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from rest_framework import routers 3 | from . import views 4 | from . import viewsets 5 | 6 | router = routers.DefaultRouter() 7 | router.register(r'users', viewsets.AccountAuthViewSet) 8 | router.register(r'register', viewsets.AccountRegisterViewSet) 9 | 10 | urlpatterns = [ 11 | path('', include(router.urls)) 12 | ] 13 | -------------------------------------------------------------------------------- /run_theme.sh: -------------------------------------------------------------------------------- 1 | docker-compose exec backend python3 manage.py loaddata setup/themes/bt.json \ 2 | && docker-compose exec backend python3 manage.py loaddata setup/themes/dj.json \ 3 | && docker-compose exec backend python3 manage.py loaddata setup/themes/fd.json \ 4 | && docker-compose exec backend python3 manage.py loaddata setup/themes/start.json \ 5 | && docker-compose exec backend python3 manage.py loaddata setup/themes/usw.json -------------------------------------------------------------------------------- /templates/errors/400.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Document 9 | 10 | 11 |

Error 400

12 | 13 | -------------------------------------------------------------------------------- /templates/errors/403.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Document 9 | 10 | 11 |

Error 403

12 | 13 | -------------------------------------------------------------------------------- /templates/errors/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Document 9 | 10 | 11 |

Error 500

12 | 13 | -------------------------------------------------------------------------------- /templates/errors/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Document 9 | 10 | 11 |

Error 404 works

12 | 13 | -------------------------------------------------------------------------------- /project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_osw4l_full project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /apps/utils/colors.py: -------------------------------------------------------------------------------- 1 | def red(msj): 2 | return "\033[0;31m{0}\033[0m".format(msj) 3 | 4 | 5 | def green(msj): 6 | return "\033[0;32m{0}\033[0m".format(msj) 7 | 8 | 9 | def orange(msj): 10 | return "\033[0;33m{0}\033[0m".format(msj) 11 | 12 | 13 | def blue(msj): 14 | return "\033[0;34m{0}\033[0m".format(msj) 15 | 16 | 17 | def purple(msj): 18 | return "\033[0;35m{0}\033[0m".format(msj) 19 | 20 | 21 | def _cyan(msj): 22 | return "\033[0;36m{0}\033[0m".format(msj) 23 | 24 | -------------------------------------------------------------------------------- /apps/utils/errors.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | 4 | def error_400(request, exception): 5 | return render(request, 'errors/400.html', status=400) 6 | 7 | 8 | def error_403(request, exception): 9 | return render(request, 'errors/403.html', status=403) 10 | 11 | 12 | def error_404(request, exception): 13 | return render(request, 'errors/404.html', status=404) 14 | 15 | 16 | def error_500(request, **kwargs): 17 | return render(request, 'errors/500.html', status=500) 18 | -------------------------------------------------------------------------------- /apps/accounts/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from .models import Account 3 | 4 | 5 | class AccountAdminForm(forms.ModelForm): 6 | class Meta: 7 | model = Account 8 | fields = ( 9 | 'username', 10 | 'email', 11 | 'first_name', 12 | 'last_name', 13 | 'validate_code', 14 | 'phone', 15 | 'role', 16 | 'deleted', 17 | 'reset_password_code', 18 | 'raw_password' 19 | ) -------------------------------------------------------------------------------- /project/asgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | from channels.auth import AuthMiddlewareStack 3 | from channels.routing import ProtocolTypeRouter, URLRouter 4 | from django.core.asgi import get_asgi_application 5 | from apps.accounts.routing import auth2_websocket_urlpatterns 6 | 7 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') 8 | 9 | application = ProtocolTypeRouter({ 10 | "http": get_asgi_application(), 11 | "websocket": AuthMiddlewareStack( 12 | URLRouter( 13 | auth2_websocket_urlpatterns 14 | ) 15 | ), 16 | }) -------------------------------------------------------------------------------- /apps/main/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Setup 3 | 4 | 5 | @admin.register(Setup) 6 | class SetupAdmin(admin.ModelAdmin): 7 | list_display = [ 8 | 'id', 9 | 'allow_register', 10 | 'disable_user_when_register', 11 | 'http_server_on', 12 | 'ws_server_on', 13 | 'twilio_key' 14 | ] 15 | 16 | def has_add_permission(self, request): 17 | return Setup.objects.count() == 0 18 | 19 | def has_delete_permission(self, request, obj=None): 20 | return False 21 | -------------------------------------------------------------------------------- /apps/utils/managers.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class ModelModelQuerySet(models.QuerySet): 5 | def all(self): 6 | return self.filter(deleted=False) 7 | 8 | def deleted(self): 9 | return self.filter(deleted=True) 10 | 11 | 12 | class ModelModelManager(models.Manager): 13 | def get_queryset(self): 14 | return ModelModelQuerySet(self.model, using=self._db) 15 | 16 | def all(self): 17 | return self.get_queryset().all() 18 | 19 | def deleted(self): 20 | return self.get_queryset().deleted() 21 | 22 | -------------------------------------------------------------------------------- /apps/utils/exceptions.py: -------------------------------------------------------------------------------- 1 | from rest_framework import status 2 | from rest_framework.exceptions import APIException 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | 6 | class EmailValidationError(APIException): 7 | status_code = status.HTTP_400_BAD_REQUEST 8 | default_detail = _('Email already exists.') 9 | default_code = 'email_exists' 10 | 11 | 12 | 13 | class RegisterDisabledValidationError(APIException): 14 | status_code = status.HTTP_400_BAD_REQUEST 15 | default_detail = _('Register disabled.') 16 | default_code = 'register_disabled' 17 | 18 | 19 | -------------------------------------------------------------------------------- /apps/utils/redis.py: -------------------------------------------------------------------------------- 1 | import json 2 | from django.conf import settings 3 | 4 | class RedisClient: 5 | @staticmethod 6 | def get_client(): 7 | return settings.REDIS 8 | 9 | def get(self, key: str): 10 | return self.get_client().get(key) 11 | 12 | def set(self, key: str, value: str): 13 | self.get_client().set(key, value) 14 | 15 | def get_json(self, key: str): 16 | bytes_data = self.get(key=key) 17 | json_data = bytes_data.decode('utf8').replace("'", '"') 18 | return json.loads(json_data) 19 | 20 | 21 | client = RedisClient() 22 | -------------------------------------------------------------------------------- /setup/nginx/api/default.conf: -------------------------------------------------------------------------------- 1 | upstream backend { 2 | server backend:8002; 3 | } 4 | 5 | map $http_upgrade $connection_upgrade { 6 | default upgrade; 7 | '' close; 8 | } 9 | 10 | server { 11 | listen 80; 12 | client_max_body_size 60M; 13 | 14 | location / { 15 | include /etc/nginx/uwsgi_params; 16 | uwsgi_pass backend; 17 | 18 | uwsgi_param Host $host; 19 | uwsgi_param X-Real-IP $remote_addr; 20 | uwsgi_param X-Forwarded-For $proxy_add_x_forwarded_for; 21 | uwsgi_param X-Forwarded-Proto $http_x_forwarded_proto; 22 | } 23 | 24 | location /static { 25 | alias /app/static; 26 | } 27 | 28 | location /media { 29 | alias /app/media; 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /apps/utils/shortcuts.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import _get_queryset 2 | from rest_framework.exceptions import ParseError, PermissionDenied 3 | 4 | 5 | def get_object_or_none(klass, *args, **kwargs): 6 | queryset = _get_queryset(klass) 7 | try: 8 | return queryset.get(*args, **kwargs) 9 | except: 10 | return None 11 | 12 | 13 | def get_list_or_none(klass, *args, **kwargs): 14 | queryset = _get_queryset(klass) 15 | obj_list = list(queryset.filter(*args, **kwargs)) 16 | if not obj_list: 17 | return None 18 | return obj_list 19 | 20 | 21 | def raise_parse_error(key=None, value=None): 22 | raise ParseError({key: value}) 23 | 24 | 25 | def raise_error(message): 26 | raise ParseError({'message': message}) 27 | 28 | 29 | def raise_permission_error(message): 30 | raise PermissionDenied({'message': message}) 31 | -------------------------------------------------------------------------------- /apps/utils/email.py: -------------------------------------------------------------------------------- 1 | from django.core.mail import EmailMultiAlternatives 2 | from django.template.loader import get_template 3 | from django.conf import settings 4 | from apps.utils.redis import client as redis 5 | 6 | 7 | def send(**kwargs): 8 | from_email = '' 9 | subject = kwargs.get('subject') 10 | to_email = kwargs.get('to_email') 11 | template = kwargs.get('template') 12 | context = kwargs.get('context') 13 | 14 | setup = redis.get_json('setup') 15 | settings.EMAIL_HOST = setup.get('email_host') 16 | settings.EMAIL_HOST_USER = setup.get('email_host_user') 17 | 18 | template = get_template(template) 19 | html_template = template.render(context) 20 | msg = EmailMultiAlternatives(subject, subject, from_email, to=[to_email]) 21 | msg.attach_alternative(html_template, "text/html") 22 | msg.send() 23 | return kwargs 24 | -------------------------------------------------------------------------------- /apps/accounts/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.hashers import make_password 2 | from django.contrib.gis.db import models 3 | from django.utils.crypto import get_random_string 4 | from django_lifecycle import AFTER_CREATE, hook, BEFORE_UPDATE 5 | from apps.utils.models import BaseModel, BaseModelUser 6 | from apps.utils.redis import client as redis 7 | 8 | 9 | class Account(BaseModelUser): 10 | phone = models.CharField( 11 | max_length=15 12 | ) 13 | 14 | class Meta: 15 | verbose_name = 'Account' 16 | verbose_name_plural = 'Accounts' 17 | 18 | @hook(AFTER_CREATE) 19 | def on_create(self): 20 | if redis.get_json('setup').get('disable_user_when_register'): 21 | self.disable() 22 | self.set_raw_password() 23 | 24 | @hook(BEFORE_UPDATE) 25 | def on_update(self): 26 | self.set_raw_password() 27 | 28 | -------------------------------------------------------------------------------- /env_template: -------------------------------------------------------------------------------- 1 | # Google 2 | GR_CAPTCHA_SECRET_KEY=xxxx 3 | 4 | # Firebase Token 5 | FCM_TOKEN=xxxx 6 | 7 | # Twilio Test Credentials 8 | TWILIO_ACCOUNT_SID=xxxx 9 | TWILIO_AUTH_TOKEN=xxx 10 | TWILIO_FROM_NUMBER=xxxx 11 | 12 | # Auth 13 | VERIFICATION_CODE_EXPIRATION_TIME=5000 14 | 15 | # AWS - Bucket 16 | AWS_STORAGE_BUCKET_NAME=x 17 | AWS_ACCESS_KEY_ID=y 18 | AWS_SECRET_ACCESS_KEY=z 19 | AWS_S3_REGION_NAME=us-east-1 20 | 21 | # Django 22 | DJANGO_SECRET_KEY=secret 23 | DJANGO_DEBUG=true 24 | DJANGO_PRODUCTION=false 25 | 26 | # email 27 | EMAIL_HOST= 28 | EMAIL_PORT=587 29 | EMAIL_HOST_USER=x 30 | EMAIL_HOST_PASSWORD=y 31 | EMAIL_USE_TLS=False 32 | EMAIL_USE_SSL=False 33 | 34 | POSTGRES_DB=db_x 35 | POSTGRES_USER=db_x 36 | PG_PORT=5432 37 | # dont touch PG_HOST 38 | PG_HOST=database 39 | POSTGRES_PASS=123 40 | # dont touch ALLOW_IP_RANGE 41 | ALLOW_IP_RANGE=0.0.0.0/0 42 | 43 | LANG=C.UTF-8 44 | LC_ALL=C.UTF-8 45 | 46 | # Maps 47 | GOOGLE_MAPS_KEY=x 48 | -------------------------------------------------------------------------------- /apps/utils/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework.permissions import BasePermission 2 | from apps.utils.exceptions import RegisterDisabledValidationError 3 | from apps.utils.redis import client as redis 4 | 5 | 6 | class IsAccount(BasePermission): 7 | """ 8 | Allows access only to account users. 9 | """ 10 | 11 | def has_permission(self, request, view): 12 | return hasattr(request.user, 'account') 13 | 14 | 15 | class IsCompanyOwner(BasePermission): 16 | """ 17 | Allows access only to account owners. 18 | """ 19 | 20 | def has_permission(self, request, view): 21 | return bool(request.user.account.role == 'owner') 22 | 23 | 24 | class IsRegisterEnabled(BasePermission): 25 | """ 26 | Allows access when register allow_register=True 27 | """ 28 | 29 | def has_permission(self, request, view): 30 | if not redis.get_json('setup').get('allow_register'): 31 | raise RegisterDisabledValidationError() 32 | return True 33 | 34 | -------------------------------------------------------------------------------- /apps/utils/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.forms import UserCreationForm 3 | from django.contrib.auth.models import User 4 | from django.utils.translation import ugettext, ugettext_lazy as _ 5 | 6 | 7 | class BaseUserCreationForm(UserCreationForm): 8 | title = None 9 | email = forms.EmailField(label=_('email address')) 10 | first_name = forms.CharField(label=_('first name')) 11 | last_name = forms.CharField(label=_('last name')) 12 | 13 | class Meta: 14 | model = User 15 | exclude = ( 16 | 'last_login', 17 | 'date_joined', 18 | 'groups', 19 | 'user_permissions', 20 | 'password', 21 | 'is_active', 22 | 'is_staff', 23 | 'is_superuser', 24 | 'username', 25 | ) 26 | fields = '__all__' 27 | 28 | 29 | class FormAllFields(forms.ModelForm): 30 | 31 | form_title = 'None' 32 | 33 | class Meta: 34 | model = None 35 | fields = '__all__' 36 | 37 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | ADD requirements.txt /app/requirements.txt 4 | 5 | WORKDIR /app/ 6 | 7 | RUN apt-get update -y && apt-get upgrade -y 8 | RUN apt-get install software-properties-common -y 9 | 10 | RUN add-apt-repository ppa:ubuntugis/ppa -y 11 | RUN apt-get update -y 12 | 13 | RUN apt-get install -y wget build-essential libpq-dev \ 14 | python3-dev libffi-dev python3-pip wget \ 15 | pkg-config libpng-dev 16 | 17 | RUN apt-get install -y binutils libproj-dev gdal-bin 18 | 19 | RUN apt-get install -y python3-setuptools python3-wheel python3-cffi libcairo2 \ 20 | libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf2.0-0 \ 21 | libffi-dev shared-mime-info 22 | 23 | RUN apt-get install -y libpq-dev gdal-bin libgdal-dev 24 | RUN export CPLUS_INCLUDE_PATH=/usr/include/gdal 25 | RUN export C_INCLUDE_PATH=/usr/include/gdal 26 | RUN pip3 install GDAL 27 | RUN pip3 install --upgrade setuptools 28 | 29 | RUN pip3 install --upgrade pip 30 | RUN pip3 install -r requirements.txt 31 | RUN export LC_ALL=es_ES.UTF-8 32 | 33 | RUN adduser --disabled-password --gecos '' app 34 | RUN chown -R app:app /app && chmod -R 755 /app 35 | 36 | ENV HOME /home/app 37 | USER app -------------------------------------------------------------------------------- /apps/accounts/consumers.py: -------------------------------------------------------------------------------- 1 | import json 2 | from asgiref.sync import async_to_sync 3 | from channels.generic.websocket import WebsocketConsumer 4 | 5 | 6 | class UserStatusConsumer(WebsocketConsumer): 7 | channel_identifier = None 8 | 9 | def connect(self): 10 | self.channel_identifier = 'orders' 11 | async_to_sync(self.channel_layer.group_add)( 12 | self.channel_identifier, 13 | self.channel_name 14 | ) 15 | self.accept() 16 | 17 | def disconnect(self, close_code): 18 | async_to_sync(self.channel_layer.group_discard)( 19 | self.channel_identifier, 20 | self.channel_name 21 | ) 22 | 23 | def receive(self, text_data=None, bytes_data=None): 24 | message = json.loads(text_data) 25 | async_to_sync(self.channel_layer.group_send)( 26 | self.channel_identifier, 27 | { 28 | 'type': 'user_status', 29 | 'message': message 30 | } 31 | ) 32 | 33 | def user_status(self, notification): 34 | message = notification['text'] 35 | self.send(text_data=json.dumps(message)) 36 | 37 | -------------------------------------------------------------------------------- /project/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include 3 | from drf_yasg.views import get_schema_view 4 | from drf_yasg import openapi 5 | from django.conf import settings 6 | from django.conf.urls.static import static 7 | from rest_framework import permissions 8 | 9 | schema_view = get_schema_view( 10 | openapi.Info( 11 | title="Osw4l Api V1", 12 | default_version='v1', 13 | description="Project build by osw4l", 14 | contact=openapi.Contact(email="ioswxd@gmail.com"), 15 | license=openapi.License(name="BSD License"), 16 | ), 17 | public=True, 18 | permission_classes=(permissions.IsAuthenticatedOrReadOnly,), 19 | ) 20 | 21 | urlpatterns = [ 22 | path('admin/', admin.site.urls), 23 | path('', schema_view.with_ui('swagger', cache_timeout=None), name='schema-swagger-ui'), 24 | path('accounts/', include('apps.accounts.urls')), 25 | ] 26 | 27 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 28 | 29 | handler400 = 'apps.utils.errors.error_400' 30 | handler403 = 'apps.utils.errors.error_403' 31 | handler404 = 'apps.utils.errors.error_404' 32 | handler500 = 'apps.utils.errors.error_500' -------------------------------------------------------------------------------- /setup/nginx/full/default.conf: -------------------------------------------------------------------------------- 1 | upstream backend { 2 | server backend:8002; 3 | } 4 | 5 | upstream websockets { 6 | server websockets:8003; 7 | } 8 | 9 | map $http_upgrade $connection_upgrade { 10 | default upgrade; 11 | '' close; 12 | } 13 | 14 | server { 15 | listen 80; 16 | client_max_body_size 60M; 17 | 18 | location / { 19 | include /etc/nginx/uwsgi_params; 20 | uwsgi_pass backend; 21 | 22 | uwsgi_param Host $host; 23 | uwsgi_param X-Real-IP $remote_addr; 24 | uwsgi_param X-Forwarded-For $proxy_add_x_forwarded_for; 25 | uwsgi_param X-Forwarded-Proto $http_x_forwarded_proto; 26 | } 27 | 28 | location /ws/ { 29 | proxy_pass http://websockets; 30 | proxy_http_version 1.1; 31 | proxy_set_header Upgrade $http_upgrade; 32 | proxy_set_header Connection $connection_upgrade; 33 | 34 | proxy_redirect off; 35 | proxy_set_header Host $host; 36 | proxy_set_header X-Real-IP $remote_addr; 37 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 38 | proxy_set_header X-Forwarded-Host $server_name; 39 | } 40 | 41 | location /static { 42 | alias /app/static; 43 | } 44 | 45 | location /media { 46 | alias /app/media; 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /apps/accounts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .forms import AccountAdminForm 4 | from .models import Account 5 | 6 | 7 | @admin.register(Account) 8 | class AccountAdmin(admin.ModelAdmin): 9 | list_display = [ 10 | 'uuid', 11 | 'username', 12 | 'date_joined', 13 | 'phone', 14 | 'role', 15 | 'is_active', 16 | 'deleted', 17 | ] 18 | list_display_links = [ 19 | 'uuid' 20 | ] 21 | form = AccountAdminForm 22 | actions = [ 23 | 'enable', 24 | 'disable', 25 | 'restore', 26 | 'logical_erase' 27 | ] 28 | search_fields = [ 29 | 'email', 30 | 'username', 31 | 'first_name', 32 | 'last_name' 33 | ] 34 | 35 | def enable(self, request, queryset): 36 | for ad in queryset: 37 | ad.enable() 38 | 39 | def disable(self, request, queryset): 40 | for ad in queryset: 41 | ad.disable() 42 | 43 | def logical_erase(self, request, queryset): 44 | for ad in queryset: 45 | ad.logical_erase() 46 | 47 | def restore(self, request, queryset): 48 | for ad in queryset: 49 | ad.restore() 50 | 51 | enable.description = 'Enable User(s)' 52 | disable.description = 'Disable User(s)' 53 | logical_erase.description = 'Delete User(s)' 54 | restore.description = 'Restore User(s)' 55 | 56 | -------------------------------------------------------------------------------- /setup/themes/bt.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "admin_interface.theme", 4 | "fields": { 5 | "name": "Bootstrap", 6 | "active": true, 7 | "title": "Django administration", 8 | "title_color": "#503873", 9 | "title_visible": false, 10 | "logo": "", 11 | "logo_color": "#503873", 12 | "logo_visible": true, 13 | "css_header_background_color": "#FFFFFF", 14 | "css_header_text_color": "#463265", 15 | "css_header_link_color": "#463265", 16 | "css_header_link_hover_color": "#7351A6", 17 | "css_module_background_color": "#7351A6", 18 | "css_module_text_color": "#FFFFFF", 19 | "css_module_link_color": "#CDBFE3", 20 | "css_module_link_hover_color": "#FFFFFF", 21 | "css_module_rounded_corners": true, 22 | "css_generic_link_color": "#463265", 23 | "css_generic_link_hover_color": "#7351A6", 24 | "css_save_button_background_color": "#5CB85C", 25 | "css_save_button_background_hover_color": "#449D44", 26 | "css_save_button_text_color": "#FFFFFF", 27 | "css_delete_button_background_color": "#D9534F", 28 | "css_delete_button_background_hover_color": "#C9302C", 29 | "css_delete_button_text_color": "#FFFFFF", 30 | "related_modal_active": true, 31 | "related_modal_background_color": "#503873", 32 | "related_modal_background_opacity": 0.2, 33 | "related_modal_rounded_corners": true, 34 | "list_filter_dropdown": false, 35 | "recent_actions_visible": true 36 | } 37 | } 38 | ] -------------------------------------------------------------------------------- /setup/themes/dj.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "admin_interface.theme", 4 | "fields": { 5 | "name": "Django", 6 | "active": true, 7 | "title": "Django administration", 8 | "title_color": "#F5DD5D", 9 | "title_visible": true, 10 | "logo": "", 11 | "logo_color": "#FFFFFF", 12 | "logo_visible": true, 13 | "css_header_background_color": "#0C4B33", 14 | "css_header_text_color": "#44B78B", 15 | "css_header_link_color": "#FFFFFF", 16 | "css_header_link_hover_color": "#C9F0DD", 17 | "css_module_background_color": "#44B78B", 18 | "css_module_text_color": "#FFFFFF", 19 | "css_module_link_color": "#FFFFFF", 20 | "css_module_link_hover_color": "#C9F0DD", 21 | "css_module_rounded_corners": true, 22 | "css_generic_link_color": "#0C3C26", 23 | "css_generic_link_hover_color": "#156641", 24 | "css_save_button_background_color": "#0C4B33", 25 | "css_save_button_background_hover_color": "#0C3C26", 26 | "css_save_button_text_color": "#FFFFFF", 27 | "css_delete_button_background_color": "#BA2121", 28 | "css_delete_button_background_hover_color": "#A41515", 29 | "css_delete_button_text_color": "#FFFFFF", 30 | "related_modal_active": true, 31 | "related_modal_background_color": "#000000", 32 | "related_modal_background_opacity": 0.2, 33 | "related_modal_rounded_corners": true, 34 | "list_filter_dropdown": false, 35 | "recent_actions_visible": true 36 | } 37 | } 38 | ] -------------------------------------------------------------------------------- /setup/themes/start.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "admin_interface.theme", 4 | "fields": { 5 | "name": "Django", 6 | "active": true, 7 | "title": "Django administration", 8 | "title_color": "#F5DD5D", 9 | "title_visible": true, 10 | "logo": "", 11 | "logo_color": "#FFFFFF", 12 | "logo_visible": true, 13 | "css_header_background_color": "#0C4B33", 14 | "css_header_text_color": "#44B78B", 15 | "css_header_link_color": "#FFFFFF", 16 | "css_header_link_hover_color": "#C9F0DD", 17 | "css_module_background_color": "#44B78B", 18 | "css_module_text_color": "#FFFFFF", 19 | "css_module_link_color": "#FFFFFF", 20 | "css_module_link_hover_color": "#C9F0DD", 21 | "css_module_rounded_corners": true, 22 | "css_generic_link_color": "#0C3C26", 23 | "css_generic_link_hover_color": "#156641", 24 | "css_save_button_background_color": "#0C4B33", 25 | "css_save_button_background_hover_color": "#0C3C26", 26 | "css_save_button_text_color": "#FFFFFF", 27 | "css_delete_button_background_color": "#BA2121", 28 | "css_delete_button_background_hover_color": "#A41515", 29 | "css_delete_button_text_color": "#FFFFFF", 30 | "related_modal_active": true, 31 | "related_modal_background_color": "#000000", 32 | "related_modal_background_opacity": 0.2, 33 | "related_modal_rounded_corners": true, 34 | "list_filter_dropdown": false, 35 | "recent_actions_visible": true 36 | } 37 | } 38 | ] -------------------------------------------------------------------------------- /setup/themes/usw.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "admin_interface.theme", 4 | "fields": { 5 | "name": "USWDS", 6 | "active": true, 7 | "title": "Django administration", 8 | "title_color": "#FFFFFF", 9 | "title_visible": false, 10 | "logo": "", 11 | "logo_color": "#FFFFFF", 12 | "logo_visible": true, 13 | "css_header_background_color": "#112E51", 14 | "css_header_text_color": "#FFFFFF", 15 | "css_header_link_color": "#FFFFFF", 16 | "css_header_link_hover_color": "#E1F3F8", 17 | "css_module_background_color": "#205493", 18 | "css_module_text_color": "#FFFFFF", 19 | "css_module_link_color": "#FFFFFF", 20 | "css_module_link_hover_color": "#E1F3F8", 21 | "css_module_rounded_corners": true, 22 | "css_generic_link_color": "#205493", 23 | "css_generic_link_hover_color": "#0071BC", 24 | "css_save_button_background_color": "#205493", 25 | "css_save_button_background_hover_color": "#112E51", 26 | "css_save_button_text_color": "#FFFFFF", 27 | "css_delete_button_background_color": "#CD2026", 28 | "css_delete_button_background_hover_color": "#981B1E", 29 | "css_delete_button_text_color": "#FFFFFF", 30 | "related_modal_active": true, 31 | "related_modal_background_color": "#000000", 32 | "related_modal_background_opacity": 0.8, 33 | "related_modal_rounded_corners": true, 34 | "list_filter_dropdown": false, 35 | "recent_actions_visible": true 36 | } 37 | } 38 | ] -------------------------------------------------------------------------------- /setup/themes/fd.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "admin_interface.theme", 4 | "fields": { 5 | "name": "Foundation", 6 | "active": true, 7 | "title": "Django administration", 8 | "title_color": "#DDDDDD", 9 | "title_visible": false, 10 | "logo": "", 11 | "logo_color": "#CCCCCC", 12 | "logo_visible": true, 13 | "css_header_background_color": "#2C3840", 14 | "css_header_text_color": "#FFFFFF", 15 | "css_header_link_color": "#FFFFFF", 16 | "css_header_link_hover_color": "#DDDDDD", 17 | "css_module_background_color": "#074E68", 18 | "css_module_text_color": "#FFFFFF", 19 | "css_module_link_color": "#FFFFFF", 20 | "css_module_link_hover_color": "#DDDDDD", 21 | "css_module_rounded_corners": true, 22 | "css_generic_link_color": "#000000", 23 | "css_generic_link_hover_color": "#074E68", 24 | "css_save_button_background_color": "#2199E8", 25 | "css_save_button_background_hover_color": "#1585CF", 26 | "css_save_button_text_color": "#FFFFFF", 27 | "css_delete_button_background_color": "#CC4B37", 28 | "css_delete_button_background_hover_color": "#BF4634", 29 | "css_delete_button_text_color": "#FFFFFF", 30 | "related_modal_active": true, 31 | "related_modal_background_color": "#000000", 32 | "related_modal_background_opacity": 0.2, 33 | "related_modal_rounded_corners": true, 34 | "list_filter_dropdown": false, 35 | "recent_actions_visible": true 36 | } 37 | } 38 | ] -------------------------------------------------------------------------------- /apps/utils/sms.py: -------------------------------------------------------------------------------- 1 | import time 2 | from twilio.rest import Client 3 | from django.conf import settings 4 | from apps.main.models import Sms 5 | from apps.utils.shortcuts import get_object_or_none 6 | from apps.utils.redis import client as redis 7 | 8 | 9 | def send_sms(phone, sms, log_id): 10 | setup = redis.get_json('setup') 11 | 12 | account_sid = setup.get('twilio_account_sid') 13 | auth_token = setup.get('twilio_auth_token') 14 | from_phone = setup.get('twilio_phone') 15 | client = Client(account_sid, auth_token) 16 | 17 | time.sleep(2) 18 | 19 | log = get_object_or_none(Sms, id=log_id) 20 | message = client.messages.create( 21 | from_=from_phone, 22 | to='{}'.format(phone), 23 | body=sms 24 | ) 25 | 26 | message = client.messages(message.sid).fetch() 27 | data = { 28 | "account_sid": message.account_sid, 29 | "api_version": message.api_version, 30 | "body": message.body, 31 | "direction": message.direction, 32 | "error_code": message.error_code, 33 | "error_message": message.error_message, 34 | "from": settings.TWILIO_FROM_NUMBER, 35 | "messaging_service_sid": message.messaging_service_sid, 36 | "num_media": message.num_media, 37 | "num_segments": message.num_segments, 38 | "price": message.price, 39 | "price_unit": message.price_unit, 40 | "sid": message.sid, 41 | "status": message.status, 42 | "to": message.to, 43 | "uri": message.uri 44 | } 45 | if log and (message.status == 'queued' or message.status == 'sent'): 46 | log.set_status(status=True, data=data) 47 | else: 48 | log.set_status(status=False, data=data) 49 | 50 | return data 51 | 52 | -------------------------------------------------------------------------------- /apps/accounts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2022-03-10 22:22 2 | 3 | import django.contrib.auth.models 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import django_lifecycle.mixins 7 | import uuid 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | ('auth', '0012_alter_user_first_name_max_length'), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='Account', 21 | fields=[ 22 | ('user_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='auth.user')), 23 | ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), 24 | ('created_at', models.DateTimeField(auto_now_add=True)), 25 | ('updated_at', models.DateTimeField(auto_now=True)), 26 | ('validate_code', models.CharField(blank=True, max_length=10, null=True)), 27 | ('role', models.CharField(blank=True, choices=[('accounting', 'Accounting'), ('owner', 'Owner'), ('manager', 'Manager'), ('hr', 'Human Resources'), ('customer', 'Customer')], default='customer', max_length=20, null=True)), 28 | ('raw_password', models.CharField(max_length=255)), 29 | ('reset_password_code', models.CharField(blank=True, max_length=6, null=True)), 30 | ('deleted', models.BooleanField(default=False)), 31 | ('phone', models.CharField(max_length=15)), 32 | ], 33 | options={ 34 | 'verbose_name': 'Account', 35 | 'verbose_name_plural': 'Accounts', 36 | }, 37 | bases=(django_lifecycle.mixins.LifecycleModelMixin, 'auth.user', models.Model), 38 | managers=[ 39 | ('objects', django.contrib.auth.models.UserManager()), 40 | ], 41 | ), 42 | ] 43 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aioredis==1.3.1 2 | amqp==5.0.9 3 | anyio==3.5.0 4 | asgiref==3.5.0 5 | async-timeout==4.0.2 6 | attrs==21.4.0 7 | autobahn==21.11.1 8 | Automat==20.2.0 9 | billiard==3.6.4.0 10 | boto3==1.20.42 11 | botocore==1.23.42 12 | Brotli==1.0.9 13 | celery==5.2.3 14 | certifi==2021.10.8 15 | cffi==1.15.0 16 | channels==3.0.4 17 | channels-redis==3.3.1 18 | charset-normalizer==2.0.10 19 | click==8.0.3 20 | click-didyoumean==0.3.0 21 | click-plugins==1.1.1 22 | click-repl==0.2.0 23 | constantly==15.1.0 24 | coreapi==2.3.3 25 | coreschema==0.0.4 26 | cssselect2==0.4.1 27 | daphne==3.0.2 28 | Deprecated==1.2.13 29 | Django==3.2 30 | django-admin-interface==0.18.5 31 | django-admin-rangefilter==0.8.3 32 | django-admin-views==0.8.0 33 | django-celery-beat==2.2.1 34 | django-celery-results==2.2.0 35 | django-colorfield==0.6.3 36 | django-cors-headers==3.11.0 37 | django-elasticsearch-dsl==7.2.0 38 | django-elasticsearch-dsl-drf==0.22 39 | django-environ==0.8.1 40 | django-extensions==3.1.5 41 | django-filter==21.1 42 | django-flat-responsive==2.0 43 | django-flat-theme==1.1.4 44 | django-geojson==3.2.0 45 | django-json-widget==1.1.1 46 | django-leaflet==0.28.2 47 | django-lifecycle==0.9.3 48 | django-map-widgets==0.3.2 49 | django-nine==0.2.5 50 | django-rest-elasticsearch==0.4.2 51 | django-smtp-ssl==1.0 52 | django-timezone-field==4.2.3 53 | djangorestframework==3.13.1 54 | djangorestframework-gis==0.18 55 | drf-yasg==1.20.0 56 | elasticsearch==7.12.0 57 | elasticsearch-dsl==7.3.0 58 | fonttools==4.29.0 59 | future==0.18.2 60 | googlemaps==4.5.3 61 | h11==0.12.0 62 | hiredis==2.0.0 63 | html5lib==1.1 64 | httpcore==0.14.5 65 | httpx==0.21.3 66 | hyperlink==21.0.0 67 | idna==3.3 68 | importlib-metadata==4.10.1 69 | incremental==21.3.0 70 | inflection==0.5.1 71 | itypes==1.2.0 72 | Jinja2==3.0.3 73 | jmespath==0.10.0 74 | kombu==5.2.3 75 | Markdown==3.3.6 76 | MarkupSafe==2.0.1 77 | msgpack==1.0.3 78 | packaging==21.3 79 | Pillow==8.0.0 80 | prompt-toolkit==3.0.24 81 | psycopg2==2.9.3 82 | psycopg2-binary==2.9.3 83 | pyasn1==0.4.8 84 | pyasn1-modules==0.2.8 85 | pydyf==0.1.2 86 | pyOpenSSL==21.0.0 87 | pyparsing==3.0.7 88 | pyphen==0.12.0 89 | python-crontab==2.6.0 90 | python-dateutil==2.8.2 91 | pytz==2021.3 92 | redis==4.1.1 93 | requests==2.27.1 94 | rfc3986==1.5.0 95 | ruamel.yaml==0.17.20 96 | ruamel.yaml.clib==0.2.6 97 | s3transfer==0.5.0 98 | sentry-sdk==1.5.4 99 | service-identity==21.1.0 100 | six==1.16.0 101 | sniffio==1.2.0 102 | sqlparse==0.4.2 103 | tinycss2==1.1.1 104 | Twisted==21.7.0 105 | txaio==21.2.1 106 | typing-extensions==4.0.1 107 | uritemplate==4.1.1 108 | urllib3==1.26.8 109 | urlman==2.0.1 110 | vine==5.0.0 111 | wcwidth==0.2.5 112 | weasyprint==54.0 113 | webencodings==0.5.1 114 | wrapt==1.13.3 115 | xlrd==1.2.0 116 | xlwt==1.3.0 117 | zipp==3.7.0 118 | zope.interface==5.4.0 119 | zopfli==0.1.9 120 | uWSGI 121 | dj-static 122 | static 123 | static3 124 | twilio==6.38.0 125 | pydantic -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | networks: 4 | webnet: 5 | redisnet: 6 | db_network: 7 | elastic_network: 8 | 9 | volumes: 10 | postgres_data: 11 | redisdata: 12 | 13 | services: 14 | elastic: 15 | image: elasticsearch:7.8.1 16 | volumes: 17 | - ./setup/docker/elastic:/usr/share/elasticsearch/data 18 | command: ["elasticsearch", "-Elogger.level=WARN"] 19 | environment: 20 | - bootstrap.memory_lock=true 21 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m" 22 | - discovery.type=single-node 23 | ports: 24 | - '9920:9200' 25 | networks: 26 | - elastic_network 27 | logging: 28 | driver: 'none' 29 | 30 | backend: 31 | container_name: django_osw4l_full 32 | restart: on-failure 33 | build: . 34 | env_file: .env 35 | command: uwsgi --socket=:8002 --module=project.wsgi:application --py-autoreload=1 36 | volumes: 37 | - .:/app 38 | - ./static:/app/static 39 | depends_on: 40 | - database 41 | - elastic 42 | networks: 43 | - webnet 44 | - redisnet 45 | - db_network 46 | - elastic_network 47 | 48 | websockets: 49 | restart: on-failure 50 | build: . 51 | command: daphne -b 0.0.0.0 -p 8003 project.asgi:application 52 | volumes: 53 | - .:/app 54 | depends_on: 55 | - database 56 | - redis 57 | networks: 58 | - webnet 59 | - redisnet 60 | - db_network 61 | 62 | redis: 63 | image: redis:latest 64 | restart: always 65 | volumes: 66 | - ./setup/docker/redis-data:/data 67 | networks: 68 | - redisnet 69 | command: redis-server 70 | 71 | database: 72 | image: kartoza/postgis:13.0 73 | volumes: 74 | - ./setup/docker/postgres:/var/lib/postgresql/13/main 75 | env_file: .env 76 | ports: 77 | - '8500:5432' 78 | restart: on-failure 79 | networks: 80 | - db_network 81 | 82 | nginx: 83 | image: nginx:1.15.0 84 | depends_on: 85 | - websockets 86 | - backend 87 | volumes: 88 | - ./setup/nginx/full:/etc/nginx/conf.d 89 | - ./static:/app/static 90 | networks: 91 | - webnet 92 | ports: 93 | - '4500:80' 94 | logging: 95 | driver: 'none' 96 | 97 | worker: 98 | build: . 99 | volumes: 100 | - .:/app 101 | env_file: .env 102 | restart: on-failure 103 | command: celery -A project worker --concurrency=10 -l info 104 | networks: 105 | - redisnet 106 | - db_network 107 | - elastic_network 108 | 109 | beat: 110 | build: . 111 | volumes: 112 | - .:/app 113 | env_file: .env 114 | restart: on-failure 115 | command: celery -A project beat -l info --scheduler django_celery_beat.schedulers:DatabaseScheduler --pidfile=/home/app/celery.pid 116 | networks: 117 | - redisnet 118 | - db_network 119 | logging: 120 | driver: 'none' 121 | -------------------------------------------------------------------------------- /apps/utils/viewsets.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets, mixins 2 | from rest_framework.pagination import PageNumberPagination 3 | from rest_framework.permissions import AllowAny, IsAuthenticated 4 | from collections import OrderedDict 5 | from rest_framework import filters 6 | from rest_framework.response import Response 7 | from .permissions import IsAccount, IsCompanyOwner 8 | 9 | 10 | class CustomPagination(PageNumberPagination): 11 | page_size = 2 12 | 13 | def get_paginated_response(self, data): 14 | return Response(OrderedDict([ 15 | ('current_page', self.page.number), 16 | ('pages', self.page.paginator.num_pages), 17 | ('count', self.page.paginator.count), 18 | ('next', self.get_next_link()), 19 | ('previous', self.get_previous_link()), 20 | ('results', data) 21 | ])) 22 | 23 | 24 | class PublicReadOnlyViewSet(viewsets.ReadOnlyModelViewSet): 25 | permission_classes = [AllowAny, ] 26 | filter_backends = (filters.SearchFilter,) 27 | 28 | def get_serializer_class(self): 29 | if self.action == 'retrieve': 30 | if hasattr(self, 'detail_serializer_class'): 31 | return self.detail_serializer_class 32 | return super().get_serializer_class() 33 | 34 | 35 | class PrivateModelViewSet(mixins.CreateModelMixin, 36 | mixins.UpdateModelMixin, 37 | mixins.ListModelMixin, 38 | mixins.RetrieveModelMixin, 39 | viewsets.GenericViewSet): 40 | permission_classes = [IsAuthenticated, ] 41 | filter_backends = (filters.SearchFilter,) 42 | 43 | def get_serializer_class(self): 44 | if self.action == 'retrieve': 45 | if hasattr(self, 'detail_serializer_class'): 46 | return self.detail_serializer_class 47 | return super().get_serializer_class() 48 | 49 | 50 | class OwnerBaseViewSet(viewsets.GenericViewSet): 51 | permission_classes = [ 52 | IsAuthenticated, 53 | IsAccount, 54 | IsCompanyOwner 55 | ] 56 | lookup_field = 'uuid' 57 | pagination_class = CustomPagination 58 | filter_backends = (filters.SearchFilter,) 59 | 60 | def get_serializer_class(self): 61 | if self.action == 'list': 62 | if hasattr(self, 'list_serializer_class'): 63 | return self.list_serializer_class 64 | if self.action == 'retrieve': 65 | if hasattr(self, 'detail_serializer_class'): 66 | return self.detail_serializer_class 67 | return super().get_serializer_class() 68 | 69 | 70 | class OwnerModelViewSet(mixins.CreateModelMixin, 71 | mixins.UpdateModelMixin, 72 | mixins.ListModelMixin, 73 | mixins.RetrieveModelMixin, 74 | OwnerBaseViewSet): 75 | pass 76 | 77 | 78 | class OwnerCreateListViewSet(mixins.CreateModelMixin, 79 | mixins.ListModelMixin, 80 | mixins.RetrieveModelMixin, 81 | OwnerBaseViewSet): 82 | pass 83 | -------------------------------------------------------------------------------- /apps/main/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2022-01-27 18:17 2 | 3 | from django.db import migrations, models 4 | import django_lifecycle.mixins 5 | 6 | 7 | def create_setup(apps, schema_editor): 8 | from apps.main.models import Setup 9 | Setup.objects.create() 10 | 11 | 12 | class Migration(migrations.Migration): 13 | 14 | initial = True 15 | 16 | dependencies = [ 17 | ] 18 | 19 | operations = [ 20 | migrations.CreateModel( 21 | name='Setup', 22 | fields=[ 23 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 24 | ('allow_register', models.BooleanField(default=False)), 25 | ('disable_user_when_register', models.BooleanField(default=True)), 26 | ('http_server_on', models.BooleanField(default=True)), 27 | ('ws_server_on', models.BooleanField(default=True)), 28 | ('twilio_key', models.CharField(blank=True, max_length=30, null=True)), 29 | ('twilio_account_sid', models.CharField(blank=True, max_length=50, null=True)), 30 | ('twilio_auth_token', models.CharField(blank=True, max_length=50, null=True)), 31 | ('twilio_phone', models.CharField(blank=True, max_length=50, null=True)), 32 | ('email_host', models.CharField(blank=True, max_length=50, null=True)), 33 | ('email_host_user', models.CharField(blank=True, max_length=50, null=True)), 34 | ('from_email', models.EmailField(blank=True, max_length=254, null=True)), 35 | ('payment_public_key', models.CharField(blank=True, max_length=50, null=True)), 36 | ('payment_private_key', models.CharField(blank=True, max_length=50, null=True)), 37 | ('payment_link_url', models.URLField(blank=True, null=True)), 38 | ('frontend_url', models.URLField(blank=True, null=True)), 39 | ('backend_url', models.URLField(blank=True, null=True)), 40 | ('test_mode', models.BooleanField(default=True)), 41 | ], 42 | options={ 43 | 'verbose_name': 'Setup', 44 | 'verbose_name_plural': 'Setup', 45 | }, 46 | bases=(django_lifecycle.mixins.LifecycleModelMixin, models.Model), 47 | ), 48 | migrations.CreateModel( 49 | name='Sms', 50 | fields=[ 51 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 52 | ('created_at', models.DateTimeField(auto_now_add=True)), 53 | ('phone', models.CharField(max_length=15)), 54 | ('sms', models.TextField()), 55 | ('success', models.BooleanField(null=True)), 56 | ('source', models.CharField(max_length=20)), 57 | ('data', models.JSONField()), 58 | ], 59 | options={ 60 | 'verbose_name': 'Sms', 61 | 'verbose_name_plural': 'Sms', 62 | }, 63 | ), 64 | migrations.RunPython( 65 | create_setup 66 | ), 67 | ] 68 | -------------------------------------------------------------------------------- /apps/utils/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from django.contrib.auth.models import UserManager 3 | from django.contrib.auth.models import User 4 | from django.contrib.gis.db import models 5 | from .managers import ModelModelManager 6 | from django_lifecycle import LifecycleModel 7 | from .choices import ROLES, CUSTOMER 8 | 9 | 10 | class BaseModel(LifecycleModel): 11 | uuid = models.UUIDField( 12 | default=uuid.uuid4, 13 | editable=False, 14 | unique=True 15 | ) 16 | created_at = models.DateTimeField(auto_now_add=True) 17 | updated_at = models.DateTimeField(auto_now=True) 18 | deleted = models.BooleanField(default=False, editable=False) 19 | objects = ModelModelManager() 20 | 21 | class Meta: 22 | abstract = True 23 | 24 | def logical_erase(self): 25 | self.deleted = True 26 | self.save(update_fields=['deleted']) 27 | return { 28 | 'deleted': self.deleted 29 | } 30 | 31 | 32 | class BaseModelUser(BaseModel, User): 33 | objects = UserManager() 34 | validate_code = models.CharField( 35 | max_length=10, 36 | blank=True, 37 | null=True 38 | ) 39 | role = models.CharField( 40 | max_length=20, 41 | choices=ROLES, 42 | default=CUSTOMER, 43 | blank=True, 44 | null=True 45 | ) 46 | raw_password = models.CharField( 47 | max_length=255 48 | ) 49 | reset_password_code = models.CharField( 50 | max_length=6, 51 | blank=True, 52 | null=True 53 | ) 54 | deleted = models.BooleanField(default=False) 55 | 56 | class Meta: 57 | abstract = True 58 | 59 | def set_raw_password(self): 60 | if self.raw_password: 61 | password = make_password(self.raw_password) 62 | self.__class__.objects.filter(id=self.id).update( 63 | password=password 64 | ) 65 | 66 | def reset_password(self, password): 67 | self.raw_password = password 68 | self.reset_password_code = None 69 | self.save(update_fields=['raw_password', 'reset_password_code']) 70 | 71 | def generate_reset_password_code(self): 72 | self.reset_password_code = get_random_string(length=6, allowed_chars='0123456789') 73 | self.save(update_fields=['reset_password_code']) 74 | 75 | def logical_erase(self): 76 | self.is_active = False 77 | self.deleted = True 78 | self.save(update_fields=['is_active', 'deleted']) 79 | return { 80 | 'deleted': self.deleted, 81 | 'disabled': not self.is_active 82 | } 83 | 84 | def disable(self): 85 | self.is_active = False 86 | self.save(update_fields=['is_active']) 87 | return { 88 | 'disabled': self.is_active 89 | } 90 | 91 | def enable(self): 92 | self.is_active = True 93 | self.save(update_fields=['is_active']) 94 | return { 95 | 'disabled': self.is_active 96 | } 97 | 98 | def restore(self): 99 | self.is_active = True 100 | self.deleted = False 101 | self.save(update_fields=['is_active', 'deleted']) 102 | 103 | 104 | class BaseNameModel(BaseModel): 105 | name = models.CharField( 106 | max_length=50, 107 | unique=True 108 | ) 109 | 110 | class Meta: 111 | abstract = True 112 | 113 | def __str__(self): 114 | return self.name 115 | -------------------------------------------------------------------------------- /apps/accounts/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import authenticate 2 | from rest_framework import serializers 3 | from rest_framework.authtoken.models import Token 4 | from .models import Account 5 | from apps.utils.shortcuts import get_object_or_none 6 | from apps.utils.exceptions import EmailValidationError 7 | 8 | 9 | class UserResetPasswordSerializer(serializers.Serializer): 10 | username = serializers.CharField(min_length=2, max_length=64) 11 | 12 | def update(self, instance, validated_data): 13 | pass 14 | 15 | def create(self, validated_data): 16 | pass 17 | 18 | 19 | class UserResetPasswordCodeSerializer(UserResetPasswordSerializer): 20 | code = serializers.CharField(max_length=6) 21 | 22 | 23 | class UserResetPasswordSetPasswordSerializer(UserResetPasswordCodeSerializer): 24 | password = serializers.CharField(min_length=6) 25 | 26 | 27 | class UserLoginSerializer(serializers.Serializer): 28 | username = serializers.CharField(min_length=2, max_length=64) 29 | password = serializers.CharField() 30 | 31 | def update(self, instance, validated_data): 32 | pass 33 | 34 | def validate(self, data): 35 | user = authenticate(username=data.get('username'), password=data.get('password')) 36 | if not user: 37 | raise serializers.ValidationError({ 38 | 'error': 'Las credenciales no son válidas' 39 | }) 40 | if not hasattr(user, 'account'): 41 | raise serializers.ValidationError({ 42 | 'error': 'No tiene permisos para entrar aquí' 43 | }) 44 | self.context['user'] = user 45 | return data 46 | 47 | def create(self, data): 48 | user = self.context['user'] 49 | token = get_object_or_none(Token, user=user) 50 | if token: 51 | token.delete() 52 | token, created = Token.objects.update_or_create(user=user) 53 | user = AccountSerializer(user.account) 54 | return user.data, token.key 55 | 56 | 57 | class CheckEmailSerializer(serializers.Serializer): 58 | email = serializers.EmailField() 59 | 60 | def validate(self, data): 61 | if Account.objects.filter(email=data.get('email')): 62 | raise EmailValidationError() 63 | return data 64 | 65 | def create(self, validated_data): 66 | pass 67 | 68 | def update(self, instance, validated_data): 69 | pass 70 | 71 | 72 | class AccountSerializer(serializers.ModelSerializer): 73 | class Meta: 74 | model = Account 75 | fields = ( 76 | 'uuid', 77 | 'username', 78 | 'phone', 79 | 'email', 80 | 'first_name', 81 | 'last_name', 82 | 'role', 83 | 'is_active' 84 | ) 85 | extra_kwargs = { 86 | 'uuid': { 87 | 'read_only': True 88 | } 89 | } 90 | 91 | 92 | class AccountRegisterSerializer(AccountSerializer): 93 | class Meta(AccountSerializer.Meta): 94 | fields = AccountSerializer.Meta.fields + ('raw_password',) 95 | extra_kwargs = { 96 | 'raw_password': { 97 | 'write_only': True 98 | }, 99 | 'role': { 100 | 'read_only': True 101 | }, 102 | 'code': { 103 | 'read_only': True 104 | }, 105 | 'is_active': { 106 | 'read_only': True 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /apps/main/models.py: -------------------------------------------------------------------------------- 1 | import json 2 | from django.contrib.gis.db import models 3 | from django_lifecycle import LifecycleModel, AFTER_CREATE, BEFORE_UPDATE, hook 4 | from apps.utils.redis import client as redis 5 | 6 | 7 | class Setup(LifecycleModel): 8 | allow_register = models.BooleanField(default=False) 9 | disable_user_when_register = models.BooleanField(default=True) 10 | http_server_on = models.BooleanField(default=True) 11 | ws_server_on = models.BooleanField(default=True) 12 | twilio_key = models.CharField( 13 | max_length=30, 14 | blank=True, 15 | null=True 16 | ) 17 | twilio_account_sid = models.CharField( 18 | max_length=50, 19 | blank=True, 20 | null=True 21 | ) 22 | twilio_auth_token = models.CharField( 23 | max_length=50, 24 | blank=True, 25 | null=True 26 | ) 27 | twilio_phone = models.CharField( 28 | max_length=50, 29 | blank=True, 30 | null=True 31 | ) 32 | email_host = models.CharField( 33 | max_length=50, 34 | blank=True, 35 | null=True 36 | ) 37 | email_host_user = models.CharField( 38 | max_length=50, 39 | blank=True, 40 | null=True 41 | ) 42 | from_email = models.EmailField( 43 | blank=True, 44 | null=True 45 | ) 46 | payment_public_key = models.CharField( 47 | max_length=50, 48 | blank=True, 49 | null=True 50 | ) 51 | payment_private_key = models.CharField( 52 | max_length=50, 53 | blank=True, 54 | null=True 55 | ) 56 | payment_link_url = models.URLField( 57 | blank=True, 58 | null=True 59 | ) 60 | frontend_url = models.URLField( 61 | blank=True, 62 | null=True 63 | ) 64 | backend_url = models.URLField( 65 | blank=True, 66 | null=True 67 | ) 68 | test_mode = models.BooleanField(default=True) 69 | 70 | 71 | class Meta: 72 | verbose_name = 'Setup' 73 | verbose_name_plural = 'Setup' 74 | 75 | def __str__(self): 76 | return 'Project Setup' 77 | 78 | def save(self, *args, **kwargs): 79 | if self.__class__.objects.all().count() <= 1: 80 | super().save(*args, **kwargs) 81 | 82 | def get_data(self): 83 | return json.dumps({ 84 | 'allow_register': self.allow_register, 85 | 'disable_user_when_register': self.disable_user_when_register, 86 | 'payment_private_key': self.payment_private_key, 87 | 'test_mode': self.test_mode, 88 | 'twilio_account_sid': self.twilio_account_sid, 89 | 'twilio_auth_token': self.twilio_auth_token, 90 | 'twilio_phone': self.twilio_phone, 91 | 'email_host': self.email_host, 92 | 'email_host_user': self.email_host_user, 93 | 'from_email': self.from_email 94 | }) 95 | 96 | @hook(AFTER_CREATE) 97 | def on_create(self): 98 | redis.set('setup', self.get_data()) 99 | 100 | @hook(BEFORE_UPDATE) 101 | def on_update(self): 102 | redis.set('setup', self.get_data()) 103 | 104 | 105 | class Sms(models.Model): 106 | created_at = models.DateTimeField( 107 | auto_now_add=True 108 | ) 109 | phone = models.CharField(max_length=15) 110 | sms = models.TextField() 111 | success = models.BooleanField(null=True) 112 | source = models.CharField( 113 | max_length=20 114 | ) 115 | data = models.JSONField() 116 | 117 | class Meta: 118 | verbose_name = 'Sms' 119 | verbose_name_plural = 'Sms' 120 | 121 | def set_status(self, status, data): 122 | self.success = status 123 | self.data = data 124 | self.save(update_fields=['success', 'data']) 125 | -------------------------------------------------------------------------------- /apps/accounts/viewsets.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets 2 | from rest_framework.generics import get_object_or_404 3 | from rest_framework.permissions import IsAuthenticated, AllowAny 4 | from rest_framework.response import Response 5 | from rest_framework.decorators import action 6 | from apps.utils.permissions import IsAccount, IsRegisterEnabled 7 | from .models import Account 8 | from apps.utils.viewsets import OwnerModelViewSet 9 | from .serializers import ( 10 | AccountSerializer, 11 | UserLoginSerializer, 12 | UserResetPasswordCodeSerializer, 13 | UserResetPasswordSerializer, 14 | UserResetPasswordSetPasswordSerializer, 15 | AccountRegisterSerializer, CheckEmailSerializer 16 | ) 17 | 18 | 19 | 20 | class AccountRegisterViewSet(viewsets.GenericViewSet): 21 | serializer_class = AccountSerializer 22 | queryset = Account.objects.filter(deleted=False) 23 | 24 | @action(detail=False, 25 | methods=['POST'], 26 | permission_classes=[AllowAny], 27 | serializer_class=CheckEmailSerializer) 28 | def check_email(self, request): 29 | """User check email.""" 30 | serializer = self.serializer_class(data=request.data) 31 | serializer.is_valid(raise_exception=True) 32 | return Response() 33 | 34 | @action(detail=False, 35 | methods=['POST'], 36 | permission_classes=[AllowAny, IsRegisterEnabled], 37 | serializer_class=AccountRegisterSerializer) 38 | def register(self, request): 39 | serializer = self.serializer_class(data=request.data) 40 | if serializer.is_valid(raise_exception=True): 41 | serializer.save() 42 | return Response(serializer.data) 43 | 44 | 45 | class AccountAuthViewSet(viewsets.GenericViewSet): 46 | serializer_class = AccountSerializer 47 | queryset = Account.objects.filter(deleted=False) 48 | permission_classes = [ 49 | IsAuthenticated, 50 | IsAccount, 51 | ] 52 | 53 | @action(detail=False, 54 | methods=['POST'], 55 | permission_classes=[AllowAny], 56 | serializer_class=UserLoginSerializer) 57 | def login(self, request): 58 | """User sign in.""" 59 | serializer = self.serializer_class(data=request.data) 60 | serializer.is_valid(raise_exception=True) 61 | user, token = serializer.save() 62 | return Response({ 63 | 'user': user, 64 | 'token': token 65 | }) 66 | 67 | @action(detail=False, 68 | methods=['POST'], 69 | permission_classes=[AllowAny], 70 | serializer_class=UserResetPasswordSerializer) 71 | def send_reset_code(self, request): 72 | username = request.data.get('username') 73 | user = get_object_or_404(Account, username=username) 74 | user.generate_reset_password_code() 75 | return Response({ 76 | "success": True 77 | }) 78 | 79 | @action(detail=False, 80 | permission_classes=[AllowAny], 81 | methods=['POST'], 82 | serializer_class=UserResetPasswordCodeSerializer) 83 | def check_reset_password_code(self, request): 84 | username = request.data.get('username') 85 | reset_password_code = request.data.get('code') 86 | get_object_or_404(Account, username=username, reset_password_code=reset_password_code) 87 | return Response({ 88 | "success": True 89 | }) 90 | 91 | @action(detail=False, 92 | methods=['POST'], 93 | permission_classes=[AllowAny], 94 | serializer_class=UserResetPasswordSetPasswordSerializer) 95 | def set_new_password(self, request): 96 | username = request.data.get('username') 97 | code = request.data.get('code') 98 | password = request.data.get('password') 99 | user = get_object_or_404(Account, username=username, reset_password_code=code) 100 | user.reset_password(password) 101 | return Response({ 102 | "success": True 103 | }) 104 | 105 | @action(detail=False, methods=['GET']) 106 | def detail_user(self, request): 107 | serializer = self.serializer_class(request.user.account) 108 | return Response(serializer.data) 109 | 110 | @action(detail=False, methods=['PUT']) 111 | def update_user(self, request): 112 | serializer = self.serializer_class(request.user.account, request.data) 113 | if serializer.is_valid(): 114 | serializer.save() 115 | return Response(serializer.data) 116 | 117 | 118 | class AccountOwnerViewSet(OwnerModelViewSet): 119 | serializer_class = AccountRegisterSerializer 120 | queryset = Account.objects.filter(deleted=False) 121 | search_fields = ('username', 'first_name', 'last_name',) 122 | 123 | @action(detail=True, methods=['DELETE'], serializer_class=None) 124 | def delete(self, request, uuid=None): 125 | result = self.get_object().logical_erase() 126 | return Response(result) 127 | 128 | @action(detail=True, methods=['POST'], serializer_class=None) 129 | def disable(self, request, uuid=None): 130 | result = self.get_object().disable() 131 | return Response(result) 132 | 133 | @action(detail=True, methods=['POST'], serializer_class=None) 134 | def enable(self, request, uuid=None): 135 | result = self.get_object().enable() 136 | return Response(result) 137 | 138 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | 2 | # Django Docker Full - by osw4l 3 | 4 | ![enter image description here](https://i.imgur.com/rsEw4yc.png) 5 | 6 | It is a beautiful **Django** image simple to configure, run and deploy, it was made with a lot of love and dedicated for humans who love django and simple things. 7 | 8 | this project contains the next libraries 9 | 10 | - Python 3.8.10 11 | - [Django==3.2](https://docs.djangoproject.com/en/4.0/releases/3.2/) 12 | - [django-admin-interface](https://github.com/fabiocaccamo/django-admin-interface) 13 | - [Channels](https://channels.readthedocs.io/en/stable/) 14 | - [Celery](https://docs.celeryproject.org/en/stable/django/first-steps-with-django.html) 15 | - [django-celery-beat](https://django-celery-beat.readthedocs.io/en/latest/) 16 | - [django-celery-results](https://github.com/celery/django-celery-results) 17 | - [django-cors-headers](https://github.com/adamchainz/django-cors-headers) 18 | - [django-environ](https://django-environ.readthedocs.io/en/latest/) 19 | - [django-extensions](https://github.com/django-extensions/django-extensions) 20 | - [drf-yasg (Swagger)](https://github.com/axnsan12/drf-yasg) 21 | - [djangorestframework](https://www.django-rest-framework.org/) 22 | - [djangorestframework-gis](https://github.com/openwisp/django-rest-framework-gis) 23 | - [django-leaflet](https://github.com/makinacorpus/django-leaflet) 24 | - [django-map-widgets](https://github.com/erdem/django-map-widgets) 25 | - psycopg2 26 | - Redis 27 | - Pillow 28 | - django-storages 29 | - boto 30 | - botocore 31 | - s3transfer 32 | 33 | and more pretty stuff like 34 | - Docker compose 35 | - [Daphne](https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/daphne/) 36 | - [UWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) (no gunicorn) 37 | - [Postgis](https://postgis.net/) as Database 38 | - [Geo Django](https://docs.djangoproject.com/en/3.2/ref/contrib/gis/) 39 | - Leaflet and Google Maps 40 | - Django Admin Themes 41 | - Celery Worker and Celery Beat 42 | - Nginx with django static files support 43 | - Static files working fine ! 44 | - AWS S3 Storage 45 | - Natural structure, **like you weren't using docker** 46 | - Production deploy steps [click here](https://gist.github.com/osw4l/cbfbfb3f7a7f42ab31fa5083b358f316) 47 | 48 | **Django Rest Framework Swagger** 49 | 50 | the project contains its own auth backend with register, login and reset password 51 | 52 | ![enter image description here](https://i.imgur.com/n2o2Fqo.png) 53 | 54 | Each endpoint contains its own serializer and swagger collection 55 | ![enter image description here](https://i.imgur.com/Ynqm69w.png) 56 | 57 | ![enter image description here](https://i.imgur.com/BlnGLVU.png) 58 | 59 | if you want to disable the register and the confirmation after register you have to go to setup in the admin 60 | 61 | [http://localhost:4500/admin/](http://localhost:4500/admin/) 62 | 63 | Go to main and then go to setup 64 | 65 | ![enter image description here](https://i.imgur.com/Q70P0FB.png) 66 | 67 | ![enter image description here](https://i.imgur.com/qbgi0dK.png) 68 | Now go to the detail 69 | 70 | I'll disabled the register for now 71 | 72 | ![enter image description here](https://i.imgur.com/WQo5C4v.png) 73 | 74 | **Then if I try to register in the register endpoint this gonna be the result** 75 | 76 | ![enter image description here](https://i.imgur.com/1H8Zxum.png) 77 | 78 | **Django Google Maps Widget** 79 | 80 | ![enter image description here](https://cloud.githubusercontent.com/assets/1518272/26807500/ad0af4ea-4a4e-11e7-87d6-632f39e438f7.gif) 81 | 82 | **Django Leaflet** 83 | 84 | ![enter image description here](https://camo.githubusercontent.com/4744043b6b90dbac1d548f4bc4fea4b82d2859867334a85b44ff119b42f905b0/68747470733a2f2f662e636c6f75642e6769746875622e636f6d2f6173736574732f3534363639322f313034383833362f37386236616439342d313039342d313165332d383664382d6333653838363236613331642e706e67) 85 | 86 | ![enter image description here](https://fle.github.io/images/012-admin-widget.png) 87 | 88 | **Django Admin custom themes by** [django-admin-interface](https://github.com/fabiocaccamo/django-admin-interface) 89 | 90 | ![enter image description here](https://user-images.githubusercontent.com/1035294/35631521-64b0cab8-06a4-11e8-8f57-c04fdfbb7e8b.gif) 91 | 92 | 93 | **Custom Commands** 94 | 95 | ![enter image description here](https://i.imgur.com/yHCPCTv.png) 96 | 97 | to use the custom commands just give permissions 98 | 99 | **Command to collect Statics** 100 | ```bash 101 | chmod +x run_collect_static.sh 102 | ``` 103 | 104 | **Command to make migrations and migrate** 105 | ```bash 106 | chmod +x run_migrate.sh 107 | ``` 108 | 109 | **Command to create super user** 110 | 111 | ```bash 112 | chmod +x run_create_user.sh 113 | ``` 114 | 115 | **Command to load django admin themes** 116 | 117 | ```bash 118 | chmod +x run_theme.sh 119 | ``` 120 | 121 | Simple and beautiful structure 122 | 123 | ![enter image description here](https://i.imgur.com/rjlx88Y.png) 124 | to run the image follow the next instructions, just for local environment 125 | 126 | ## Create Environment file 127 | ```bash 128 | cp env_template .env 129 | ``` 130 | ## Build image 131 | 132 | ```bash 133 | docker-compose build 134 | ``` 135 | ## Up image 136 | ```bash 137 | docker-compose up -d 138 | ``` 139 | ## Migrations 140 | 141 | you can create migrations and migrate the new models changes using the custom commands 142 | 143 | **this command just run migrate command** 144 | ```bash 145 | docker-compose exec backend python3 manage.py migrate 146 | ``` 147 | 148 | **this command just run makemigrations and migrate commands** 149 | ```bash 150 | ./run_migrate.sh 151 | ``` 152 | 153 | ## Restart Celery Beat 154 | ```bash 155 | docker-compose restart beat 156 | ``` 157 | ## Create Superuser 158 | 159 | **command** 160 | ```bash 161 | docker-compose exec backend python3 manage.py createsuperuser 162 | ``` 163 | 164 | **sh file** 165 | ```bash 166 | ./run_create_user.sh 167 | ``` 168 | 169 | ## collect statics 170 | 171 | this command just **works** in **local** doesn't work in production 172 | ```bash 173 | docker-compose exec backend python3 manage.py collectstatic 174 | ``` 175 | 176 | this command **works** in **local** and production 177 | 178 | **sh file** 179 | ```bash 180 | ./run_collect_static.sh 181 | ``` 182 | 183 | ## Load Django Admin Themes 184 | 185 | ```bash 186 | ./run_theme.sh 187 | ``` 188 | 189 | ## Pycharm Support first, we need to setup the common stuff to active the autocomplete adding the Django Support choosing the manage.py and settings.py files location. 190 | 191 | ![enter image description here](https://i.imgur.com/yxaLtUc.png) 192 | now we need add the python interpreter what live inside the docker container to the project 193 | 194 | Go to preferences and to click in Interpreter then in Project Interpreter and press add 195 | 196 | ![enter image description here](https://i.imgur.com/DwKsssx.png) 197 | now, do click in Docker, select the image what contains the project name, then write python3 and press ok 198 | 199 | ![enter image description here](https://i.imgur.com/pI86DZb.png) 200 | press apply and ok, done!. 201 | 202 | ![enter image description here](https://i.imgur.com/lmpULSQ.png) 203 | now we have configured the interpreter what lives inside our Docker Container in our project 204 | 205 | Please, DON'T UPDATE THE DEPENDENCIES ! **unless necessary** 206 | 207 | if you wanna deploy this project in production, [go to here](https://gist.github.com/osw4l/cbfbfb3f7a7f42ab31fa5083b358f316) 208 | 209 | **Thanks for using my project, if you need something else, feel you free to contact me** **ioswxd@gmail.com** 210 | 211 | ## Enjoy the project 🥳 🟡 🔵 💛 💙 💟 212 | -------------------------------------------------------------------------------- /project/settings.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import os 3 | import environ 4 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 5 | BASE_DIR = Path(__file__).resolve().parent.parent 6 | 7 | 8 | # Loading enviroment 9 | ROOT_DIR = environ.Path(__file__) - 1 10 | ENV_DIR = environ.Path(__file__) - 2 11 | env = environ.Env() 12 | env.read_env(ENV_DIR('.env')) 13 | 14 | 15 | # Quick-start development settings - unsuitable for production 16 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 17 | 18 | # SECURITY WARNING: keep the secret key used in production secret! 19 | SECRET_KEY = env('DJANGO_SECRET_KEY') 20 | # SECURITY WARNING: don't run with debug turned on in production! 21 | DEBUG = env('DJANGO_DEBUG') 22 | PRODUCTION = env.bool('DJANGO_PRODUCTION', False) 23 | 24 | ALLOWED_HOSTS = ['*'] 25 | 26 | # Application definition 27 | 28 | INSTALLED_APPS = [ 29 | 'admin_interface', 30 | 'colorfield', 31 | 'channels', 32 | 'django.contrib.admin', 33 | 'django.contrib.auth', 34 | 'django.contrib.contenttypes', 35 | 'django.contrib.sessions', 36 | 'django.contrib.messages', 37 | 'django.contrib.staticfiles', 38 | 'django.contrib.gis', 39 | # third party apps 40 | 'rest_framework', 41 | 'rest_framework.authtoken', 42 | 'django_elasticsearch_dsl', 43 | 'django_elasticsearch_dsl_drf', 44 | 'rest_framework_gis', 45 | 'rangefilter', 46 | 'django_celery_beat', 47 | 'django_celery_results', 48 | 'django_extensions', 49 | 'drf_yasg', 50 | 'mapwidgets', 51 | 'leaflet', 52 | 'django_json_widget', 53 | # apps 54 | 'apps.accounts', 55 | 'apps.main', 56 | 'apps.utils', 57 | ] 58 | 59 | MIDDLEWARE = [ 60 | 'django.middleware.security.SecurityMiddleware', 61 | 'django.contrib.sessions.middleware.SessionMiddleware', 62 | 'corsheaders.middleware.CorsMiddleware', 63 | 'django.middleware.common.CommonMiddleware', 64 | 'django.middleware.csrf.CsrfViewMiddleware', 65 | 'corsheaders.middleware.CorsPostCsrfMiddleware', 66 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 67 | 'django.contrib.messages.middleware.MessageMiddleware', 68 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 69 | ] 70 | 71 | ROOT_URLCONF = 'project.urls' 72 | 73 | TEMPLATES = [ 74 | { 75 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 76 | 'DIRS': [os.path.join(BASE_DIR, 'templates')], 77 | 'APP_DIRS': True, 78 | 'OPTIONS': { 79 | 'context_processors': [ 80 | 'django.template.context_processors.debug', 81 | 'django.template.context_processors.request', 82 | 'django.contrib.auth.context_processors.auth', 83 | 'django.contrib.messages.context_processors.messages', 84 | ], 85 | }, 86 | }, 87 | ] 88 | 89 | WSGI_APPLICATION = 'project.wsgi.application' 90 | 91 | 92 | # Database 93 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 94 | 95 | DATABASES = { 96 | 'default': { 97 | 'ENGINE': 'django.contrib.gis.db.backends.postgis', 98 | 'NAME': env('POSTGRES_DB'), 99 | 'USER': env('POSTGRES_USER'), 100 | 'PASSWORD': env('POSTGRES_PASS'), 101 | 'AUTOCOMMIT': True, 102 | 'HOST': env('PG_HOST'), 103 | 'PORT': env('PG_PORT'), 104 | } 105 | } 106 | 107 | # Password validation 108 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 109 | 110 | AUTH_PASSWORD_VALIDATORS = [] 111 | 112 | 113 | # Internationalization 114 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 115 | 116 | LANGUAGE_CODE = 'en-us' 117 | 118 | TIME_ZONE = 'UTC' 119 | 120 | USE_I18N = True 121 | 122 | USE_L10N = True 123 | 124 | USE_TZ = True 125 | 126 | 127 | # Static files (CSS, JavaScript, Images) 128 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 129 | 130 | STATIC_URL = '/static/' 131 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 132 | # STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static'),) 133 | 134 | MEDIA_URL = '/media/' 135 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 136 | 137 | # Default primary key field type 138 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 139 | 140 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 141 | 142 | # extra setup 143 | 144 | CORS_ORIGIN_ALLOW_ALL = True 145 | CORS_ALLOW_HEADERS = [ 146 | 'accept', 147 | 'accept-encoding', 148 | 'authorization', 149 | 'content-type', 150 | 'dnt', 151 | 'origin', 152 | 'user-agent', 153 | 'x-csrftoken', 154 | 'x-requested-with' 155 | ] 156 | CORS_ORIGIN_WHITELIST = () 157 | X_FRAME_OPTIONS = 'SAMEORIGIN' 158 | SILENCED_SYSTEM_CHECKS = ['security.W019'] 159 | 160 | # smtp 161 | EMAIL_HOST = env('EMAIL_HOST') 162 | EMAIL_HOST_USER = env('EMAIL_HOST_USER') 163 | EMAIL_PORT = 587 164 | EMAIL_USE_TLS = True 165 | 166 | # celery 167 | CELERY_BROKER_URL = 'redis://redis:6379' 168 | CELERY_RESULT_BACKEND = 'django-db' 169 | CELERY_ACCEPT_CONTENT = ['json'] 170 | CELERY_TASK_SERIALIZER = 'json' 171 | 172 | # elastic 173 | ELASTICSEARCH_DSL = { 174 | 'default': { 175 | 'hosts': env.str('ELASTICSEARCH_DSL_HOSTS', '0.0.0.0:9200') 176 | }, 177 | } 178 | 179 | # channels 180 | ASGI_APPLICATION = 'project.asgi.application' 181 | CHANNEL_LAYERS = { 182 | 'default': { 183 | 'BACKEND': 'channels_redis.core.RedisChannelLayer', 184 | 'CONFIG': { 185 | 'hosts': [('redis', 6379)], 186 | }, 187 | }, 188 | } 189 | 190 | # drf 191 | REST_FRAMEWORK = { 192 | 'DEFAULT_AUTHENTICATION_CLASSES': [ 193 | 'rest_framework.authentication.TokenAuthentication', 194 | 'rest_framework.authentication.BasicAuthentication', 195 | 'rest_framework.authentication.SessionAuthentication', 196 | ], 197 | 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', 198 | 'DEFAULT_PERMISSION_CLASSES': [ 199 | 'rest_framework.permissions.IsAuthenticatedOrReadOnly', 200 | ], 201 | 'COERCE_DECIMAL_TO_STRING': False 202 | } 203 | 204 | # AWS S3 - Bucket 205 | DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' 206 | AWS_AUTO_CREATE_BUCKET = True 207 | AWS_S3_FILE_OVERWRITE = False 208 | AWS_STORAGE_BUCKET_NAME = env('AWS_STORAGE_BUCKET_NAME') 209 | AWS_ACCESS_KEY_ID = env('AWS_ACCESS_KEY_ID') 210 | AWS_SECRET_ACCESS_KEY = env('AWS_SECRET_ACCESS_KEY') 211 | AWS_S3_REGION_NAME = env('AWS_S3_REGION_NAME') 212 | 213 | # map widgets 214 | MAP_WIDGETS = { 215 | "GooglePointFieldWidget": ( 216 | ("zoom", 15), 217 | ("mapCenterLocationName", "bogota"), 218 | ("GooglePlaceAutocompleteOptions", {'componentRestrictions': {'country': 'co'}}), 219 | ("markerFitZoom", 12), 220 | ), 221 | "GOOGLE_MAP_API_KEY": env('GOOGLE_MAPS_KEY') 222 | } 223 | 224 | 225 | import redis 226 | REDIS = redis.Redis( 227 | host='redis', 228 | port=6379 229 | ) 230 | 231 | if PRODUCTION: 232 | AUTH_PASSWORD_VALIDATORS = [ 233 | { 234 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 235 | }, 236 | { 237 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 238 | }, 239 | { 240 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 241 | }, 242 | { 243 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 244 | }, 245 | ] 246 | SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 247 | CORS_ORIGIN_WHITELIST = () 248 | LOGGING = { 249 | 'version': 1, 250 | 'disable_existing_loggers': False, 251 | 'handlers': { 252 | 'console': { 253 | 'level': 'INFO', 254 | 'class': 'logging.StreamHandler' 255 | }, 256 | }, 257 | 'loggers': { 258 | 'django': { 259 | 'level': 'INFO', 260 | 'handlers': ['console'], 261 | }, 262 | }, 263 | } 264 | 265 | --------------------------------------------------------------------------------