├── 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 | 
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 | 
53 |
54 | Each endpoint contains its own serializer and swagger collection
55 | 
56 |
57 | 
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 | 
66 |
67 | 
68 | Now go to the detail
69 |
70 | I'll disabled the register for now
71 |
72 | 
73 |
74 | **Then if I try to register in the register endpoint this gonna be the result**
75 |
76 | 
77 |
78 | **Django Google Maps Widget**
79 |
80 | 
81 |
82 | **Django Leaflet**
83 |
84 | 
85 |
86 | 
87 |
88 | **Django Admin custom themes by** [django-admin-interface](https://github.com/fabiocaccamo/django-admin-interface)
89 |
90 | 
91 |
92 |
93 | **Custom Commands**
94 |
95 | 
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 | 
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 | 
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 | 
197 | now, do click in Docker, select the image what contains the project name, then write python3 and press ok
198 |
199 | 
200 | press apply and ok, done!.
201 |
202 | 
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 |
--------------------------------------------------------------------------------