├── tests ├── __init__.py ├── fixtures │ ├── __init__.py │ ├── deploy_fixture.json │ └── product_fixture.json ├── success │ ├── __init__.py │ ├── test_api_generator.py │ ├── test_readme_generator.py │ ├── test_admin_generator.py │ ├── test_test_generator.py │ └── test_model_generator.py └── settings.py ├── products ├── models │ ├── __init__.py │ ├── category.py │ ├── discount.py │ └── product.py ├── tests │ ├── __init__.py │ ├── test_category.py │ ├── test_discount.py │ └── test_product.py ├── migrations │ └── __init__.py ├── views.py ├── __init__.py ├── apps.py ├── services.py ├── api │ ├── urls.py │ ├── serializers.py │ └── views.py ├── signals.py ├── admin.py └── mixins.py ├── sage_painless ├── __init__.py ├── classes │ ├── __init__.py │ ├── signal.py │ ├── admin.py │ └── model.py ├── utils │ ├── __init__.py │ ├── json_service.py │ ├── timing_service.py │ ├── yaml_service.py │ ├── pep8_service.py │ ├── git_service.py │ ├── jinja_service.py │ ├── file_service.py │ ├── report_service.py │ ├── package_manager_service.py │ └── comment_service.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── validate_diagram.py │ │ ├── docs.py │ │ ├── deploy.py │ │ └── generate.py ├── services │ ├── __init__.py │ ├── base.py │ ├── constants.py │ ├── nginx_generator.py │ ├── uwsgi_generator.py │ ├── gunicorn_generator.py │ ├── admin_generator.py │ ├── readme_generator.py │ ├── test_generator.py │ ├── tox_generator.py │ ├── api_generator.py │ └── docker_generator.py ├── templates │ ├── __init__.py │ ├── uwsgi.jinja │ ├── __init__.jinja │ ├── env.jinja │ ├── tox.jinja │ ├── services.jinja │ ├── apps.jinja │ ├── setup.jinja │ ├── serializers.jinja │ ├── urls.jinja │ ├── Dockerfile.jinja │ ├── coveragerc.jinja │ ├── nginx.jinja │ ├── signals.jinja │ ├── admin.jinja │ ├── conf.jinja │ ├── docker-compose.jinja │ ├── test_model.jinja │ ├── models.jinja │ ├── test_api.jinja │ ├── README.jinja │ ├── views.jinja │ ├── mixins.jinja │ └── test.jinja ├── validators │ ├── __init__.py │ └── setting_validator.py ├── apps.py └── docs │ └── diagrams │ ├── article_diagram.json │ └── product_diagram.json ├── MANIFEST.in ├── docs ├── images │ ├── mehran.png │ ├── sepehr.jpeg │ ├── tag_docs.png │ ├── tag_pypi.png │ ├── tag_sage.png │ ├── tag_test.png │ ├── tag_django.png │ ├── tag_pypi copy.png │ ├── tag_pypi_0.0.8.png │ ├── tag_pypi_0.4.2.png │ ├── tag_python-01.png │ ├── tag_python-02.png │ └── tag_pypi copy 2.png ├── Makefile ├── make.bat ├── contribute.rst ├── quick_start.rst ├── conf.py ├── index.rst ├── faq.rst └── usage.rst ├── appveyor.yml ├── tox.ini ├── requirements.txt ├── tests_manage.py ├── AUTHORS.md ├── .coveragerc ├── setup.cfg ├── setup.py ├── CONTRIBUTING.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CHANGELOG.md ├── README.rst └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /products/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /products/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sage_painless/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/success/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /products/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sage_painless/classes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sage_painless/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sage_painless/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sage_painless/services/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sage_painless/templates/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sage_painless/validators/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sage_painless/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include sage_painless/templates/*.jinja 2 | include README.md -------------------------------------------------------------------------------- /products/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /docs/images/mehran.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sageteamorg/django-sage-painless/HEAD/docs/images/mehran.png -------------------------------------------------------------------------------- /docs/images/sepehr.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sageteamorg/django-sage-painless/HEAD/docs/images/sepehr.jpeg -------------------------------------------------------------------------------- /docs/images/tag_docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sageteamorg/django-sage-painless/HEAD/docs/images/tag_docs.png -------------------------------------------------------------------------------- /docs/images/tag_pypi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sageteamorg/django-sage-painless/HEAD/docs/images/tag_pypi.png -------------------------------------------------------------------------------- /docs/images/tag_sage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sageteamorg/django-sage-painless/HEAD/docs/images/tag_sage.png -------------------------------------------------------------------------------- /docs/images/tag_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sageteamorg/django-sage-painless/HEAD/docs/images/tag_test.png -------------------------------------------------------------------------------- /docs/images/tag_django.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sageteamorg/django-sage-painless/HEAD/docs/images/tag_django.png -------------------------------------------------------------------------------- /docs/images/tag_pypi copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sageteamorg/django-sage-painless/HEAD/docs/images/tag_pypi copy.png -------------------------------------------------------------------------------- /docs/images/tag_pypi_0.0.8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sageteamorg/django-sage-painless/HEAD/docs/images/tag_pypi_0.0.8.png -------------------------------------------------------------------------------- /docs/images/tag_pypi_0.4.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sageteamorg/django-sage-painless/HEAD/docs/images/tag_pypi_0.4.2.png -------------------------------------------------------------------------------- /docs/images/tag_python-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sageteamorg/django-sage-painless/HEAD/docs/images/tag_python-01.png -------------------------------------------------------------------------------- /docs/images/tag_python-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sageteamorg/django-sage-painless/HEAD/docs/images/tag_python-02.png -------------------------------------------------------------------------------- /docs/images/tag_pypi copy 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sageteamorg/django-sage-painless/HEAD/docs/images/tag_pypi copy 2.png -------------------------------------------------------------------------------- /sage_painless/templates/uwsgi.jinja: -------------------------------------------------------------------------------- 1 | # Automatically generated with ❤️ by django-sage-painless 2 | [uwsgi] 3 | {%for key in config%} 4 | {{key}}={{config.get(key)}} 5 | {%endfor%} -------------------------------------------------------------------------------- /products/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated __init__.py 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | default_app_config = 'products.apps.ProductsConfig' 6 | -------------------------------------------------------------------------------- /sage_painless/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SagePainlessConfig(AppConfig): 5 | name = 'sage_painless' 6 | verbose_name = 'Django Sage Painless' 7 | -------------------------------------------------------------------------------- /sage_painless/templates/__init__.jinja: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated __init__.py 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | default_app_config = '{{app_name}}.apps.{{app_name.capitalize()}}Config' -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # appveyor.yml 2 | --- 3 | environment: 4 | matrix: 5 | - TOXENV: py39 6 | 7 | build: off 8 | 9 | install: 10 | - pip install tox 11 | - pip install coverage 12 | 13 | test_script: 14 | - tox -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py39 3 | 4 | [testenv] 5 | description = Unit tests 6 | deps = pytest 7 | coverage 8 | commands = 9 | coverage run tests_manage.py test tests 10 | coverage report 11 | coverage html 12 | setenv = 13 | DJANGO_SETTINGS_MODULE=tests.settings 14 | PYTHONPATH={toxinidir} -------------------------------------------------------------------------------- /products/apps.py: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated apps.py 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | from django.apps import AppConfig 6 | 7 | 8 | class ProductsConfig(AppConfig): 9 | default_auto_field = 'django.db.models.BigAutoField' 10 | name = 'products' 11 | 12 | def ready(self): 13 | import products.signals 14 | 15 | -------------------------------------------------------------------------------- /products/services.py: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated services.py 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | 6 | from django.core.cache import cache 7 | 8 | 9 | def clear_cache_for_model(cache_key: str): 10 | """ 11 | removes all caches of this model 12 | """ 13 | keys = cache.keys(f'*{cache_key}*') 14 | cache.delete_many(keys) 15 | -------------------------------------------------------------------------------- /sage_painless/utils/json_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - Json Handling Class 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | import json 7 | 8 | 9 | class JsonHandler: 10 | @classmethod 11 | def load_json(cls, path): 12 | with open(path) as json_file: 13 | data = json.load(json_file) 14 | return data 15 | -------------------------------------------------------------------------------- /sage_painless/templates/env.jinja: -------------------------------------------------------------------------------- 1 | # Automatically generated with ❤️ by django-sage-painless 2 | 3 | # postgres 4 | POSTGRES_USER=postgres 5 | POSTGRES_PASSWORD={{random_postgres_password}} 6 | POSTGRES_DB=db 7 | {% if config.get('rabbitmq') %} 8 | # rabbitmq 9 | RABBITMQ_DEFAULT_USER=rabbitmq 10 | RABBITMQ_DEFAULT_PASS={{random_rabbitmq_password}} 11 | RABBITMQ_DEFAULT_VHOST=/ 12 | {% endif %} -------------------------------------------------------------------------------- /sage_painless/templates/tox.jinja: -------------------------------------------------------------------------------- 1 | # Automatically generated with ❤️ by django-sage-painless 2 | [tox] 3 | envlist = py39 4 | 5 | [testenv] 6 | description = unit tests 7 | deps = pytest 8 | coverage 9 | commands = 10 | coverage run manage.py test 11 | coverage report 12 | coverage html 13 | setenv = 14 | DJANGO_SETTINGS_MODULE={{kernel_name}}.settings 15 | PYTHONPATH={toxinidir} -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django>=3.2.3 2 | django-redis==4.12.1 3 | drf-yasg 4 | django-seed 5 | Faker==5.6.0 6 | redis==3.5.3 7 | djangorestframework>=3.12.4 8 | setuptools>=56.2.0 9 | autopep8>=1.5.7 10 | Jinja2>=3.0.1 11 | tox==3.24.0 12 | GitPython 13 | django-sage-encrypt 14 | django-sage-streaming 15 | coverage 16 | gunicorn 17 | greenlet 18 | gevent 19 | docutils==0.16 20 | django-filter==21.1 21 | PyYAML==6.0 -------------------------------------------------------------------------------- /sage_painless/templates/services.jinja: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated services.py 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | {% if cache_support %} 6 | from django.core.cache import cache 7 | 8 | def clear_cache_for_model(cache_key: str): 9 | """ 10 | removes all caches of this model 11 | """ 12 | keys = cache.keys(f'*{cache_key}*') 13 | cache.delete_many(keys) 14 | {% endif %} -------------------------------------------------------------------------------- /sage_painless/templates/apps.jinja: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated apps.py 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | from django.apps import AppConfig 6 | 7 | 8 | class {{app_name.capitalize()}}Config(AppConfig): 9 | default_auto_field = 'django.db.models.BigAutoField' 10 | name = '{{app_name}}' 11 | {%if signal_support%} 12 | def ready(self): 13 | import {{app_name}}.signals 14 | {%endif%} -------------------------------------------------------------------------------- /sage_painless/utils/timing_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - Timing Class 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | 7 | 8 | class TimingService: 9 | 10 | def __init__(self, *args, **kwargs): 11 | super().__init__(*args, *kwargs) 12 | 13 | @classmethod 14 | def calculate_execute_time(cls, start, end): 15 | """calculate time taken""" 16 | return (end - start) * 1000.0 17 | -------------------------------------------------------------------------------- /sage_painless/templates/setup.jinja: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated setup.py 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | from setuptools import setup, find_packages 6 | 7 | setup( 8 | name='{{kernel_name}}', 9 | packages=find_packages(), 10 | version='{{config.get('version', '1.0.0')}}', 11 | description='{{config.get('description')}}', 12 | author='{{config.get('author')}}', 13 | install_requires={{reqs}} 14 | ) -------------------------------------------------------------------------------- /sage_painless/utils/yaml_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - YAML File Handling Class 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | import yaml 7 | 8 | 9 | class Yaml: 10 | @classmethod 11 | def load_diagram(cls, diagram_path): 12 | """ 13 | load db diagram 14 | """ 15 | with open(diagram_path, 'r') as f: 16 | diagram = yaml.load_all(f.read(), Loader=yaml.FullLoader) 17 | return list(diagram) 18 | -------------------------------------------------------------------------------- /sage_painless/classes/signal.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - Signal Class 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | 7 | 8 | class Signal: 9 | def __init__(self): 10 | self.model_b = None 11 | self.model_a = None 12 | self.method = None 13 | self.field = None 14 | 15 | def set_signal(self, method, sender, dest, field): 16 | self.method = method 17 | self.model_a = sender 18 | self.model_b = dest 19 | self.field = field 20 | -------------------------------------------------------------------------------- /sage_painless/classes/admin.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - Admin Class 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | 7 | 8 | class Admin: 9 | 10 | def __init__(self): 11 | self.model = None 12 | self.list_display = None 13 | self.list_filter = None 14 | self.search_fields = None 15 | self.raw_id_fields = None 16 | self.filter_horizontal = None 17 | self.filter_vertical = None 18 | self.has_add_permission = True 19 | self.has_change_permission = True 20 | self.has_delete_permission = True 21 | -------------------------------------------------------------------------------- /products/api/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated urls.py 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | 6 | from rest_framework.routers import DefaultRouter 7 | 8 | from products.api.views import ( 9 | CategoryViewset, 10 | ProductViewset, 11 | DiscountViewset, 12 | 13 | ) 14 | 15 | router = DefaultRouter() 16 | 17 | router.register(r'category', CategoryViewset, basename='category') 18 | 19 | router.register(r'product', ProductViewset, basename='product') 20 | 21 | router.register(r'discount', DiscountViewset, basename='discount') 22 | 23 | urlpatterns = router.urls 24 | -------------------------------------------------------------------------------- /sage_painless/utils/pep8_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - Pep8 Class 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | import autopep8 7 | 8 | 9 | class Pep8: 10 | @classmethod 11 | def fix_pep8(cls, file_path): 12 | """ 13 | fix pep8 (E122,E271,E231,E261,E225,E303,E302,E305,E501,W292,W391) 14 | """ 15 | options = autopep8.parse_args( 16 | ['--in-place', '--aggressive', '--select', 'E122,E271,E231,E261,E225,E303,E302,E305,E501,W292,W391', file_path] 17 | ) 18 | autopep8.fix_file(filename=file_path, options=options) 19 | -------------------------------------------------------------------------------- /sage_painless/templates/serializers.jinja: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated serializers.py 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | from rest_framework.serializers import ModelSerializer 6 | {% for model in models %} 7 | from {{app_name}}.models.{{model.name.lower()}} import {{model.name}} 8 | {% endfor %} 9 | 10 | {% for model in models %} 11 | class {{model.name}}Serializer(ModelSerializer): 12 | """ 13 | {{model.name}} Serializer 14 | Auto generated 15 | """ 16 | class Meta: 17 | model = {{model.name}} 18 | fields = [ 19 | 'id', 20 | {%for field in model.fields%}'{{field.name}}', 21 | {% endfor %} 22 | ] 23 | {% endfor %} -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /sage_painless/templates/urls.jinja: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated urls.py 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | {% if streaming_support %} 6 | from django.urls import include, path 7 | {% endif %} 8 | from rest_framework.routers import DefaultRouter 9 | 10 | from {{app_name}}.api.views import ( 11 | {% for model in models %}{{model.name}}Viewset, 12 | {% endfor %} 13 | ) 14 | 15 | router = DefaultRouter() 16 | {% for model in models %} 17 | router.register(r'{{model.name.lower()}}', {{model.name}}Viewset, basename='{{model.name.lower()}}') 18 | {% endfor %} 19 | urlpatterns = router.urls 20 | {% if streaming_support %} 21 | urlpatterns.append(path('', include('sage_stream.api.urls'))) 22 | {% endif %} -------------------------------------------------------------------------------- /sage_painless/validators/setting_validator.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - Settings Validator Class 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | from django.db import connection 7 | 8 | 9 | class SettingValidator: 10 | def __init__(self, *args, **kwargs): 11 | super().__init__(*args, **kwargs) 12 | 13 | @classmethod 14 | def validate_pgcrypto_config(cls): 15 | """validate database settings for encryption 16 | django-sage-encrypt just works in PostgreSQL 17 | check default db 18 | """ 19 | default_vendor = connection.vendor 20 | if not default_vendor == 'postgresql': 21 | raise AssertionError('encryption just works on PostgreSQL.') 22 | -------------------------------------------------------------------------------- /tests_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', 'tests.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 | -------------------------------------------------------------------------------- /sage_painless/templates/Dockerfile.jinja: -------------------------------------------------------------------------------- 1 | # Automatically generated with ❤️ by django-sage-painless 2 | 3 | # pull official base image 4 | FROM python:3.9 5 | 6 | # set environment variables 7 | ENV PYTHONDONTWRITEBYTECODE 1 8 | ENV PYTHONUNBUFFERED 1 9 | 10 | # set work directory 11 | RUN mkdir /project 12 | WORKDIR /project 13 | COPY . /project 14 | 15 | # install dependencies 16 | RUN pip install --upgrade pip 17 | RUN pip install -r requirements.txt 18 | 19 | # migrate django project 20 | RUN python manage.py makemigrations 21 | RUN python manage.py migrate 22 | RUN python manage.py collectstatic 23 | 24 | {% if gunicorn %} 25 | RUN pip install gunicorn gevent 26 | CMD ["gunicorn", "-c", "gunicorn-conf.py", "--preload", "--bind", ":8000", "--chdir", "{{kernel_name}}", "{{kernel_name}}.wsgi:application"] 27 | {% endif %} -------------------------------------------------------------------------------- /sage_painless/templates/coveragerc.jinja: -------------------------------------------------------------------------------- 1 | # Automatically generated with ❤️ by django-sage-painless 2 | 3 | [run] 4 | branch = True 5 | include = {% for app_name in app_names %} 6 | {{app_name}}/* 7 | {% endfor %} 8 | 9 | [report] 10 | # Regexes for lines to exclude from consideration 11 | exclude_lines = 12 | # Have to re-enable the standard pragma 13 | pragma: no cover 14 | 15 | # Don't complain about missing debug-only code: 16 | def __repr__ 17 | if self\.debug 18 | 19 | # Don't complain if tests don't hit defensive assertion code: 20 | raise AssertionError 21 | raise NotImplementedError 22 | 23 | ignore_errors = True 24 | 25 | [html] 26 | directory = coverage_html_report 27 | 28 | [paths] 29 | source = {% for app_name in app_names %} 30 | {{app_name}}/ 31 | {% endfor %} -------------------------------------------------------------------------------- /sage_painless/templates/nginx.jinja: -------------------------------------------------------------------------------- 1 | # Automatically generated with ❤️ by django-sage-painless 2 | upstream backend { 3 | server backend:8000; # set for dockerized project (if your project is not dockerized change this part) 4 | } 5 | 6 | server { 7 | listen 80; 8 | server_name example.com; # you need to specify your domain 9 | client_max_body_size 100M; 10 | 11 | location / { 12 | proxy_pass http://backend; 13 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 14 | proxy_set_header Host $host; 15 | proxy_redirect off; 16 | } 17 | location /static/ { 18 | alias {{static_files}}; 19 | } 20 | location /media/ { 21 | alias {{media_files}}; 22 | } 23 | 24 | access_log /var/log/nginx/access.log; 25 | error_log /var/log/nginx/error.log; 26 | } -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | ## django-sage-painless was originally created in May 2021 2 | #### Here is an inevitably incomplete list of MUCH-APPRECIATED CONTRIBUTORS 3 | 4 | | Name | Role | Status | Email | 5 | | ----------------------------- |:------------------------:|:--------------------------:|:--------------------------:| 6 | | Sepehr Akbarzadeh | Team Lead | In-Progress | sepehr@gmail.com | 7 | | Mehran Rahmanzadeh | Developer | In-Progress | mrhnz13@gmail.com | 8 | 9 | A big THANK-YOU goes to: 10 | 11 | SageTeam for letting us open-source [project name]. 12 | 13 | Guido van Rossum for creating Python. 14 | 15 | Django Software Foundation for creating Django. -------------------------------------------------------------------------------- /sage_painless/utils/git_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - Git Integration Class 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | from git import Repo 7 | 8 | 9 | class GitSupport: 10 | def __init__(self, *args, **kwargs): 11 | super().__init__(*args, **kwargs) 12 | self.repo = None 13 | 14 | def check_init(self): 15 | """check repo is initialized""" 16 | assert self.repo 17 | 18 | def init_repo(self, path, bare=False): 19 | """init git repo in given path""" 20 | repo = Repo.init(path=path, bare=bare) 21 | self.repo = repo 22 | return repo 23 | 24 | def commit_file(self, file_path, commit_message): 25 | """add file & commit it""" 26 | self.check_init() 27 | self.repo.index.add([file_path]) 28 | self.repo.index.commit(commit_message) 29 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | include = sage_painless/* 4 | 5 | [report] 6 | # Regexes for lines to exclude from consideration 7 | exclude_lines = 8 | # Have to re-enable the standard pragma 9 | pragma: no cover 10 | 11 | # Don't complain about missing debug-only code: 12 | def __repr__ 13 | if self\.debug 14 | 15 | # Don't complain if tests don't hit defensive assertion code: 16 | raise AssertionError 17 | raise NotImplementedError 18 | 19 | # Don't complain if non-runnable code isn't run: 20 | else 21 | elif 22 | if 23 | self.* = 24 | self.stream_to_template 25 | for model in models 26 | for app_name 27 | self.fix_pep8 28 | end_time = time.time() 29 | return False 30 | return self.name 31 | 32 | ignore_errors = True 33 | 34 | [html] 35 | directory = coverage_html_report 36 | 37 | [paths] 38 | source = 39 | sage_painless/ -------------------------------------------------------------------------------- /tests/fixtures/deploy_fixture.json: -------------------------------------------------------------------------------- 1 | { 2 | "deploy": { 3 | "docker": { 4 | "redis": true, 5 | "rabbitmq": false 6 | }, 7 | "gunicorn": { 8 | "bind": "0.0.0.0:8000", 9 | "accesslog": "/var/log/gunicorn/gunicorn-access.log", 10 | "errorlog": "/var/log/gunicorn/gunicorn-error.log", 11 | "reload": true 12 | }, 13 | "uwsgi": { 14 | "chdir": "/src/kernel", 15 | "home": "/src/venv", 16 | "module": "kernel.wsgi", 17 | "master": true, 18 | "pidfile": "/tmp/project-master.pid", 19 | "vacuum": true, 20 | "max-requests": 3000, 21 | "processes": 10, 22 | "daemonize": "/var/log/uwsgi/uwsgi.log" 23 | }, 24 | "tox": { 25 | "version": "1.0.0", 26 | "description": "test project", 27 | "author": "SageTeam", 28 | "req_path": "requirements.txt" 29 | }, 30 | "package_manager": { 31 | "type": "pip" 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = django-sage-painless 3 | description = create django projects without coding 4 | author = Sage Team 5 | author_email = mail@sageteam.org 6 | license = GNU 7 | url = 'https://github.com/sageteam-org/django-sage-painless', 8 | classifiers = 9 | Environment :: Web Environment 10 | Framework :: Django 11 | Framework :: Django :: 3.1 12 | Framework :: Django :: 3.2 13 | Intended Audience :: Developers 14 | License :: OSI Approved :: GNU Affero General Public License v3 15 | Operating System :: OS Independent 16 | Programming Language :: Python 17 | Programming Language :: Python :: 3 18 | Programming Language :: Python :: 3 :: Only 19 | Programming Language :: Python :: 3.6 20 | Programming Language :: Python :: 3.7 21 | Programming Language :: Python :: 3.8 22 | Programming Language :: Python :: 3.9 23 | Topic :: Internet :: WWW/HTTP 24 | Topic :: Internet :: WWW/HTTP :: Dynamic Content 25 | 26 | [options] 27 | include_package_data = true 28 | python_requires = >=3.8 -------------------------------------------------------------------------------- /sage_painless/utils/jinja_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - Jinja Templating Main Class 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | from jinja2 import Environment 7 | 8 | 9 | class JinjaHandler: 10 | env = Environment(autoescape=False, optimized=False) 11 | 12 | def load_template(self, template_path): 13 | """ 14 | load jinja template from template_path 15 | """ 16 | with open(template_path, 'r', encoding='utf8') as t: 17 | template = self.env.from_string(t.read()) 18 | return template 19 | 20 | def stream_to_template(self, output_path, template_path, data=None): 21 | """ 22 | generate output file using template and data 23 | """ 24 | template = self.load_template(template_path) 25 | with open(output_path, 'w', encoding='utf8') as o: 26 | if data: 27 | template.stream( 28 | **data 29 | ).dump(o) 30 | else: 31 | template.stream().dump(o) 32 | -------------------------------------------------------------------------------- /sage_painless/management/commands/validate_diagram.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - validate_diagram management command 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | from django.core.management.base import BaseCommand 7 | 8 | from sage_painless.utils.json_service import JsonHandler 9 | from sage_painless.validators.diagram_validator import DiagramValidator 10 | 11 | 12 | class Command(BaseCommand, JsonHandler, DiagramValidator): 13 | help = 'Generate all files need to your new apps.' 14 | 15 | def add_arguments(self, parser): 16 | """initialize arguments""" 17 | parser.add_argument('-d', '--diagram', type=str, help='sql diagram path', required=True) 18 | 19 | def handle(self, *args, **options): 20 | """get configs from user and generate""" 21 | diagram_path = options.get('diagram') 22 | diagram = self.load_json(diagram_path) 23 | 24 | self.validate_all(diagram) 25 | 26 | self.stdout.write( 27 | self.style.SUCCESS('system check completed with [0] error') 28 | ) 29 | -------------------------------------------------------------------------------- /products/models/category.py: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated models.py 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | from django.db import models 6 | from django.utils.translation import ugettext_lazy as _ 7 | 8 | 9 | # cache support 10 | from products.mixins import ModelCacheMixin 11 | 12 | 13 | class Category(models.Model, ModelCacheMixin): 14 | """ 15 | Category Model 16 | Auto generated 17 | """ 18 | 19 | CACHE_KEY = 'category' # auto generated CACHE_KEY 20 | 21 | title = models.CharField( 22 | max_length=255, 23 | unique=True, 24 | 25 | ) 26 | 27 | created = models.DateTimeField( 28 | auto_now_add=True, 29 | 30 | ) 31 | 32 | modified = models.DateTimeField( 33 | auto_now=True, 34 | 35 | ) 36 | 37 | def __str__(self): 38 | return f"Category {self.pk}" 39 | 40 | class Meta: 41 | verbose_name = _("Category") # auto generated verbose_name 42 | verbose_name_plural = _("Categories") 43 | -------------------------------------------------------------------------------- /products/signals.py: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated signals.py 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | 6 | from django.db.models import signals 7 | 8 | 9 | from products.models.category import Category 10 | 11 | from products.models.product import Product 12 | 13 | from products.models.discount import Discount 14 | 15 | from products.services import clear_cache_for_model 16 | 17 | 18 | def category_clear_cache(sender, **kwargs): 19 | clear_cache_for_model(sender.CACHE_KEY) 20 | 21 | 22 | signals.post_save.connect(category_clear_cache, sender=Category) 23 | signals.pre_delete.connect(category_clear_cache, sender=Category) 24 | 25 | 26 | def product_clear_cache(sender, **kwargs): 27 | clear_cache_for_model(sender.CACHE_KEY) 28 | 29 | 30 | signals.post_save.connect(product_clear_cache, sender=Product) 31 | signals.pre_delete.connect(product_clear_cache, sender=Product) 32 | 33 | 34 | def discount_clear_cache(sender, **kwargs): 35 | clear_cache_for_model(sender.CACHE_KEY) 36 | 37 | 38 | signals.post_save.connect(discount_clear_cache, sender=Discount) 39 | signals.pre_delete.connect(discount_clear_cache, sender=Discount) 40 | -------------------------------------------------------------------------------- /products/models/discount.py: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated models.py 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | from django.db import models 6 | from django.utils.translation import ugettext_lazy as _ 7 | 8 | 9 | # cache support 10 | from products.mixins import ModelCacheMixin 11 | 12 | 13 | from products.models.product import Product 14 | 15 | 16 | class Discount(models.Model, ModelCacheMixin): 17 | """ 18 | Discount Model 19 | Auto generated 20 | """ 21 | 22 | CACHE_KEY = 'discount' # auto generated CACHE_KEY 23 | 24 | product = models.ForeignKey( 25 | to=Product, 26 | related_name='discounts', 27 | on_delete=models.CASCADE, 28 | 29 | ) 30 | 31 | discount = models.IntegerField( 32 | 33 | ) 34 | 35 | created = models.DateTimeField( 36 | auto_now_add=True, 37 | 38 | ) 39 | 40 | modified = models.DateTimeField( 41 | auto_now=True, 42 | 43 | ) 44 | 45 | def __str__(self): 46 | return f"Discount {self.pk}" 47 | 48 | class Meta: 49 | verbose_name = _("Discount") # auto generated verbose_name 50 | verbose_name_plural = _("Discounts") 51 | -------------------------------------------------------------------------------- /docs/contribute.rst: -------------------------------------------------------------------------------- 1 | Contribute 2 | =========== 3 | 4 | Project Detail 5 | -------------- 6 | 7 | You can find all technologies we used in our project into these files: 8 | 9 | - Version: 1.0.0 10 | - Frameworks: Django 3.2.4 11 | - Libraries: 12 | - Django rest framework 3.12.4 13 | - Jinja2 3.0.1 14 | - Language: Python 3.9.4 15 | 16 | Git Rules 17 | --------- 18 | 19 | Sage team Git Rules Policy is available here: 20 | 21 | - `Sage Git 22 | Policy `__ 23 | 24 | Development 25 | ----------- 26 | 27 | Run project tests before starting to develop - ``products`` app is 28 | required for running tests 29 | 30 | .. code:: shell 31 | 32 | $ python manage.py startapp products 33 | 34 | .. code:: python 35 | 36 | INSTALLED_APPS = [ 37 | ... 38 | 'products', 39 | ... 40 | ] 41 | 42 | - you have to generate everything for this app 43 | - diagram file is available here: 44 | `Diagram `__ 45 | 46 | .. code:: shell 47 | 48 | $ python manage.py generate --app products --diagram sage_painless/tests/diagrams/product_diagram.json 49 | 50 | - run tests 51 | 52 | .. code:: shell 53 | 54 | $ python manage.py test sage_painless 55 | 56 | -------------------------------------------------------------------------------- /sage_painless/templates/signals.jinja: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated signals.py 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | {% if signal_support %} 6 | from django.db.models import signals 7 | {% if cache_support %} 8 | {% for model in models %} 9 | from {{app_name}}.models.{{model.name.lower()}} import {{model.name}} 10 | {% endfor %} 11 | from {{app_name}}.services import clear_cache_for_model 12 | {% else %} 13 | {% for signal in signals %} 14 | from {{app_name}}.models.{{signal.model_a.lower()}} import {{signal.model_a}} 15 | from {{app_name}}.models.{{signal.model_b.lower()}} import {{signal.model_b}} 16 | {% endfor %} 17 | {% endif %} 18 | 19 | {% for model in models %} 20 | {% if cache_support %} 21 | def {{model.name.lower()}}_clear_cache(sender, **kwargs): 22 | clear_cache_for_model(sender.CACHE_KEY) 23 | signals.post_save.connect({{model.name.lower()}}_clear_cache, sender={{model.name}}) 24 | signals.pre_delete.connect({{model.name.lower()}}_clear_cache, sender={{model.name}}) 25 | {% endif %} 26 | {% endfor %} 27 | {% for signal in signals %} 28 | def {{signal.model_b.lower()}}_post_save(sender, instance, created, **kwargs): 29 | if created: 30 | {{signal.model_a}}.objects.create({{signal.field}}=instance) 31 | signals.post_save.connect({{signal.model_b.lower()}}_post_save, sender={{signal.model_b}}) 32 | {% endfor %} 33 | {% endif %} -------------------------------------------------------------------------------- /tests/success/test_api_generator.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.test import TestCase 4 | 5 | from sage_painless.services.api_generator import APIGenerator 6 | from sage_painless.utils.json_service import JsonHandler 7 | 8 | from tests import fixtures 9 | 10 | 11 | class TestAPIGenerator(TestCase): 12 | def setUp(self) -> None: 13 | self.json_handler = JsonHandler() 14 | self.app_name = 'products' 15 | self.api_generator = APIGenerator() 16 | self.diagram_path = os.path.abspath(fixtures.__file__).replace('__init__.py', 'product_fixture.json') 17 | self.diagram = self.json_handler.load_json(self.diagram_path).get('apps').get(self.app_name).get('models') 18 | 19 | def get_table_names(self): 20 | return self.diagram.keys() 21 | 22 | def get_table_field_names(self, table_name): 23 | return self.diagram.get(table_name).get('fields').keys() 24 | 25 | def test_extract_models(self): 26 | models = self.api_generator.extract_models(self.diagram) 27 | 28 | self.assertEqual(len(models), len(self.get_table_names())) 29 | 30 | for model in models: 31 | self.assertIn(model.name, self.get_table_names()) 32 | for field in model.fields: 33 | self.assertIn( 34 | field.name, self.get_table_field_names(model.name) 35 | ) 36 | 37 | 38 | -------------------------------------------------------------------------------- /sage_painless/utils/file_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - File Handling Class 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | import os 7 | from pathlib import Path 8 | 9 | from django.conf import settings 10 | from django.core import management 11 | 12 | 13 | class FileService: 14 | 15 | def __init__(self, *args, **kwargs): 16 | super().__init__(*args, **kwargs) 17 | 18 | @classmethod 19 | def create_app_if_not_exists(cls, app_name): 20 | if not os.path.exists(f'{settings.BASE_DIR}/{app_name}/'): 21 | management.call_command('startapp', app_name) 22 | 23 | @classmethod 24 | def create_dir_if_not_exists(cls, directory): 25 | if not os.path.exists(f'{settings.BASE_DIR}/{directory}'): 26 | os.mkdir(f'{settings.BASE_DIR}/{directory}') 27 | 28 | @classmethod 29 | def create_file_if_not_exists(cls, file_path): 30 | if not os.path.isfile(file_path): 31 | file = Path(file_path) 32 | file.touch(exist_ok=True) 33 | 34 | @classmethod 35 | def delete_file_if_exists(cls, file_path): 36 | if os.path.isfile(file_path): 37 | os.remove(file_path) 38 | 39 | @classmethod 40 | def create_dir_for_app_if_not_exists(cls, directory, app_name): 41 | if not os.path.exists(f'{settings.BASE_DIR}/{app_name}/{directory}'): 42 | os.mkdir(f'{settings.BASE_DIR}/{app_name}/{directory}') 43 | -------------------------------------------------------------------------------- /sage_painless/services/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - Base Generator Class 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | from abc import ABC, abstractmethod 7 | 8 | 9 | class BaseGenerator(ABC): 10 | """Generator Abstract""" 11 | 12 | @abstractmethod 13 | def generate(self, *args, **kwargs): 14 | """generate process 15 | serialize data & push to jinja2 template 16 | """ 17 | raise NotImplementedError('Should implement in subclass') 18 | 19 | def set_template(self, template_key, template_file): 20 | """set template 21 | template file should be in sage_painless/templates folder 22 | """ 23 | setattr(self, f'template_{template_key}', template_file) 24 | 25 | def get_template(self, template_key): 26 | if hasattr(self, f'template_{template_key}'): 27 | return getattr(self, f'template_{template_key}') 28 | raise NameError(f'template key {template_key} is not defined') 29 | 30 | def register_method(self, name): 31 | """register a method in class 32 | registered method should implement in subclass 33 | e.g: 34 | normalize_models(self, diagram): 35 | return diagram.keys() 36 | """ 37 | def _method(self, *args, **kwargs): 38 | raise NotImplementedError('Registered method should implement in subclass') 39 | setattr(BaseGenerator, name, _method) 40 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='django-sage-painless', 5 | packages=find_packages(exclude=['tests*']), 6 | package_data={'sage_painless/templates': ['*.jinja']}, 7 | include_package_data=True, 8 | version='1.14.3', 9 | description='A handy tool for generating Django-based backend projects without coding. On the other hand, ' 10 | 'it is a code generator of the Django framework.', 11 | long_description=open('README.md').read(), 12 | long_description_content_type='text/markdown', 13 | author='Sage Team', 14 | author_email='mail@sageteam.org', 15 | url='https://github.com/sageteam-org/django-sage-painless', 16 | download_url='https://github.com/sageteam-org/django-sage-painless/archive/refs/tags/1.5.0.tar.gz', 17 | keywords=[ 18 | 'django', 'python', 'generate', 19 | 'code generator', 'django rest framework', 20 | 'nginx', 'redis', 'rabbitmq', 'docker', 'gunicorn', 'automation'], 21 | install_requires=[ 22 | 'Django', 23 | 'django-redis', 24 | 'django-filter', 25 | 'redis', 26 | 'drf-yasg', 27 | 'django-seed', 28 | 'Faker', 29 | 'autopep8', 30 | 'django-sage-encrypt', 31 | 'django-sage-streaming', 32 | 'tox', 33 | 'coverage', 34 | 'gunicorn', 35 | 'greenlet', 36 | 'gevent', 37 | 'GitPython' 38 | ] 39 | ) 40 | -------------------------------------------------------------------------------- /sage_painless/templates/admin.jinja: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated admin.py 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | from django.contrib import admin 6 | {% for admin in admins %} 7 | from {{app_name}}.models.{{admin.model.lower()}} import {{admin.model}} 8 | {% endfor %} 9 | {% for admin in admins %} 10 | @admin.register({{admin.model}}) 11 | class {{admin.model}}Admin(admin.ModelAdmin): 12 | """ 13 | {{admin.model}} Admin 14 | Auto generated 15 | """ 16 | {%if admin.list_display%}list_display = {{admin.list_display}}{%endif%} 17 | 18 | {%if admin.list_filter%}list_filter = {{admin.list_filter}}{%endif%} 19 | 20 | {%if admin.search_fields%}search_fields = {{admin.search_fields}}{%endif%} 21 | 22 | {%if admin.raw_id_fields%}raw_id_fields = {{admin.raw_id_fields}}{%endif%} 23 | 24 | {%if admin.filter_horizontal%}filter_horizontal = {{admin.filter_horizontal}}{%endif%} 25 | 26 | {%if admin.filter_vertical%}filter_vertical = {{admin.filter_vertical}}{%endif%} 27 | 28 | def has_add_permission(self, *args, **kwargs): 29 | {%if admin.has_add_permission%}return True{%else%}return False{%endif%} 30 | 31 | def has_change_permission(self, *args, **kwargs): 32 | {%if admin.has_change_permission%}return True{%else%}return False{%endif%} 33 | 34 | def has_delete_permission(self, *args, **kwargs): 35 | {%if admin.has_delete_permission%}return True{%else%}return False{%endif%} 36 | {% endfor %} -------------------------------------------------------------------------------- /products/api/serializers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated serializers.py 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | from rest_framework.serializers import ModelSerializer 6 | 7 | from products.models.category import Category 8 | 9 | from products.models.product import Product 10 | 11 | from products.models.discount import Discount 12 | 13 | 14 | class CategorySerializer(ModelSerializer): 15 | """ 16 | Category Serializer 17 | Auto generated 18 | """ 19 | class Meta: 20 | model = Category 21 | fields = [ 22 | 'id', 23 | 'title', 24 | 'created', 25 | 'modified', 26 | 27 | ] 28 | 29 | 30 | class ProductSerializer(ModelSerializer): 31 | """ 32 | Product Serializer 33 | Auto generated 34 | """ 35 | class Meta: 36 | model = Product 37 | fields = [ 38 | 'id', 39 | 'title', 40 | 'description', 41 | 'price', 42 | 'category', 43 | 'created', 44 | 'modified', 45 | 46 | ] 47 | 48 | 49 | class DiscountSerializer(ModelSerializer): 50 | """ 51 | Discount Serializer 52 | Auto generated 53 | """ 54 | class Meta: 55 | model = Discount 56 | fields = [ 57 | 'id', 58 | 'product', 59 | 'discount', 60 | 'created', 61 | 'modified', 62 | 63 | ] 64 | -------------------------------------------------------------------------------- /products/models/product.py: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated models.py 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | from django.db import models 6 | from django.utils.translation import ugettext_lazy as _ 7 | 8 | 9 | # cache support 10 | from products.mixins import ModelCacheMixin 11 | 12 | 13 | from products.models.category import Category 14 | 15 | 16 | class Product(models.Model, ModelCacheMixin): 17 | """ 18 | Product Model 19 | Auto generated 20 | """ 21 | 22 | CACHE_KEY = 'product' # auto generated CACHE_KEY 23 | 24 | title = models.CharField( 25 | max_length=255, 26 | 27 | ) 28 | 29 | description = models.CharField( 30 | max_length=255, 31 | 32 | ) 33 | 34 | price = models.IntegerField( 35 | 36 | ) 37 | 38 | category = models.ForeignKey( 39 | to=Category, 40 | related_name='products', 41 | on_delete=models.CASCADE, 42 | 43 | ) 44 | 45 | created = models.DateTimeField( 46 | auto_now_add=True, 47 | 48 | ) 49 | 50 | modified = models.DateTimeField( 51 | auto_now=True, 52 | 53 | ) 54 | 55 | def __str__(self): 56 | return f"Product {self.pk}" 57 | 58 | class Meta: 59 | verbose_name = _("Product") # auto generated verbose_name 60 | verbose_name_plural = _("Products") 61 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute to django-sage-painless 2 | 3 | ## Did you find a bug? 4 | 5 | - Ensure the bug was not already reported by searching on GitHub under Issues 6 | - If you're unable to find an open issue addressing the problem, open a new one. Be sure to include a title and clear description, as much relevant information as possible, and a code sample or an executable test case demonstrating the expected behavior that is not occurring. 7 | 8 | ## Did you write a patch that fixes a bug? 9 | 10 | - Open a new GitHub pull request with the patch. 11 | - Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. 12 | 13 | ## Did you fix whitespace, format code, or make a purely cosmetic patch? 14 | 15 | Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of [project name] will generally not be accepted (read more about our rationales behind this decision). 16 | 17 | ## Do you intend to add a new feature or change an existing one? 18 | 19 | - Suggest your change in the and start writing code. 20 | - Do not open an issue on GitHub until you have collected positive feedback about the change. GitHub issues are primarily intended for bug reports and fixes. 21 | 22 | ## Do you have questions about the source code? 23 | 24 | - Ask any question about how to use django-sage-painless in the [sage team slack]. 25 | 26 | django-sage-painless is a volunteer effort. We encourage you to pitch in and join the team! 27 | 28 | S.A.G.E. Team 29 | -------------------------------------------------------------------------------- /sage_painless/templates/conf.jinja: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated conf.py 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | import multiprocessing 6 | 7 | {% if comments.get('bind') %}# {{comments.get('bind')}}{%endif%} 8 | bind = '{{config.pop('bind', '0.0.0.0:8000')}}' 9 | 10 | {% if comments.get('backlog') %}# {{comments.get('backlog')}}{%endif%} 11 | backlog = {{config.pop('backlog', '2084')}} 12 | 13 | {% if comments.get('workers') %}# {{comments.get('workers')}}{%endif%} 14 | workers = {{config.pop('workers', 'multiprocessing.cpu_count() * 2 + 1')}} 15 | 16 | {% if comments.get('worker_class') %}# {{comments.get('worker_class')}}{%endif%} 17 | worker_class = '{{config.pop('worker_class', 'gevent')}}' 18 | 19 | {% if comments.get('worker_connections') %}# {{comments.get('worker_connections')}}{%endif%} 20 | worker_connections = {{config.pop('worker_connections', '1000')}} 21 | 22 | {% if comments.get('timeout') %}# {{comments.get('timeout')}}{%endif%} 23 | timeout = {{config.pop('timeout', '30')}} 24 | 25 | {% if comments.get('loglevel') %}# {{comments.get('loglevel')}}{%endif%} 26 | loglevel = '{{config.pop('loglevel', 'info')}}' 27 | 28 | {% if comments.get('accesslog') %}# {{comments.get('accesslog')}}{%endif%} 29 | accesslog = '{{config.pop('accesslog', '-')}}' 30 | 31 | {% if comments.get('errorlog') %}# {{comments.get('errorlog')}}{%endif%} 32 | errorlog = '{{config.pop('errorlog', '-')}}' 33 | {% for key in config %} 34 | {% if comments.get(key) %}# {{comments.get(key)}}{%endif%} 35 | {{key}}={% if config.get(key) is string %}'{{config.get(key)}}'{%else%}{{config.get(key)}}{%endif%} 36 | {%endfor%} -------------------------------------------------------------------------------- /sage_painless/templates/docker-compose.jinja: -------------------------------------------------------------------------------- 1 | # Automatically generated with ❤️ by django-sage-painless 2 | version: "{{docker_config.get('compose_version', '3')}}" 3 | 4 | services: 5 | backend: 6 | container_name: backend 7 | build: 8 | context: . 9 | dockerfile: Dockerfile 10 | restart: always 11 | volumes: 12 | - .:/project 13 | {% if gunicorn %} 14 | - /var/log/gunicorn/gunicorn-access.log:/var/log/gunicorn/gunicorn-access.log 15 | - /var/log/gunicorn/gunicorn-error.log:/var/log/gunicorn/gunicorn-error.log 16 | {% endif %} 17 | networks: 18 | - pgsql_network 19 | depends_on: 20 | - pgsql_db 21 | {% if docker_config.get('redis') %} 22 | - redis 23 | {% endif %} 24 | {% if docker_config.get('rabbitmq') %} 25 | - rabbitmq 26 | {% endif %} 27 | 28 | pgsql_db: 29 | image: postgres:latest 30 | restart: always 31 | container_name: pgsql_db 32 | volumes: 33 | - pgsql_data:/var/lib/postgresql/data/ 34 | env_file: 35 | - .env.prod 36 | networks: 37 | - pgsql_network 38 | 39 | {% if docker_config.get('redis') %} 40 | redis: 41 | restart: always 42 | container_name: redis 43 | image: redis:latest 44 | networks: 45 | - pgsql_network 46 | {% endif %} 47 | 48 | {% if docker_config.get('rabbitmq') %} 49 | rabbitmq: 50 | container_name: rabbitmq 51 | image: rabbitmq:3-management-alpine 52 | env_file: 53 | - .env.prod 54 | {% endif %} 55 | 56 | volumes: 57 | pgsql_data: 58 | external: true 59 | networks: 60 | pgsql_network: 61 | external: true -------------------------------------------------------------------------------- /sage_painless/services/constants.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - Generator Constants Observer Class 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | from abc import ABC 7 | 8 | 9 | class GeneratorConstants(ABC): 10 | """Generator Constant Variables""" 11 | MODELS_KEYWORD = 'models' 12 | APPS_KEYWORD = 'apps' 13 | FIELDS_KEYWORD = 'fields' 14 | TYPE_KEYWORD = 'type' 15 | ENCRYPTED_KEYWORD = 'encrypt' 16 | STREAM_KEYWORD = 'stream' 17 | VALIDATORS_KEYWORD = 'validators' 18 | FUNC_KEYWORD = 'func' 19 | ARG_KEYWORD = 'arg' 20 | ADMIN_KEYWORD = 'admin' 21 | API_KEYWORD = 'api' 22 | API_DIR = 'api' 23 | TESTS_DIR = 'tests' 24 | DEPLOY_KEYWORD = 'deploy' 25 | DOCKER_KEYWORD = 'docker' 26 | GUNICORN_KEYWORD = 'gunicorn' 27 | SPACE = ' ' 28 | BRANCH = '│ ' 29 | TEE = '├── ' 30 | LAST = '└── ' 31 | IGNORE_DIRS = [ 32 | 'venv', '.pytest_cache', 33 | '.tox', '.vscode', 'dist', 34 | '.git', '__pycache__', '_build', 35 | '_static', '_templates', 36 | ] 37 | TOX_KEYWORD = 'tox' 38 | UWSGI_KEYWORD = 'uwsgi' 39 | PERMISSION_KEYWORD = 'permission' 40 | METHODS_KEYWORD = 'methods' 41 | FILTER_KEYWORD = 'filter' 42 | SEARCH_KEYWORD = 'search' 43 | 44 | def get_constant(self, name): 45 | """get constant from variables""" 46 | if hasattr(self, name): 47 | return getattr(self, name) 48 | raise NameError(f'name {name} is not defined') 49 | 50 | def set_constant(self, name, value): 51 | """set constant variable in subclass""" 52 | return setattr(self, name, value) 53 | -------------------------------------------------------------------------------- /docs/quick_start.rst: -------------------------------------------------------------------------------- 1 | Quick Start 2 | =========== 3 | 4 | Getting Started 5 | --------------- 6 | 7 | Before creating django project you must first create virtualenv. 8 | 9 | .. code:: shell 10 | 11 | $ python3.9 -m pip install virtualenv 12 | $ python3.9 -m virtualenv venv 13 | 14 | To activate virtual environment in ubuntu: 15 | 16 | .. code:: shell 17 | 18 | $ source venv/bin/activate 19 | 20 | To deactivate virtual environment use: 21 | 22 | .. code:: shell 23 | 24 | $ deactivate 25 | 26 | Start Project 27 | ------------- 28 | 29 | First create a Django project 30 | 31 | .. code:: shell 32 | 33 | $ mkdir GeneratorTutorials 34 | $ cd GeneratorTutorials 35 | $ django-admin startproject kernel . 36 | 37 | Next we have to create a sample app that we want to generate code for it 38 | (it is required for development. you will run tests on this app) 39 | 40 | .. code:: shell 41 | 42 | $ python manage.py startapp products 43 | 44 | Now we have to add 'products' to INSTALLED\_APPS in settings.py 45 | 46 | .. code:: python 47 | 48 | INSTALLED_APPS = [ 49 | ... 50 | 'products', 51 | ... 52 | ] 53 | 54 | Install Generator 55 | ----------------- 56 | 57 | First install package 58 | 59 | .. code:: shell 60 | 61 | $ pip install django-sage-painless 62 | 63 | Then add 'sage\_painless' to INSTALLED\_APPS in settings.py 64 | 65 | These apps should be in your INSTALLED\_APPS: 66 | 67 | - rest\_framework 68 | - drf\_yasg 69 | - django\_seed 70 | 71 | .. code:: python 72 | 73 | INSTALLED_APPS = [ 74 | ... 75 | 'sage_painless', 76 | ... 77 | 'rest_framework', 78 | 'drf_yasg', 79 | 'django_seed', 80 | ... 81 | ] 82 | -------------------------------------------------------------------------------- /tests/success/test_readme_generator.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from django.conf import settings 4 | from django.test import TestCase 5 | from django.apps import apps 6 | 7 | from sage_painless.services.readme_generator import ReadMeGenerator 8 | 9 | 10 | class TestReadMeGenerator(TestCase): 11 | def setUp(self) -> None: 12 | self.readme_generator = ReadMeGenerator() 13 | 14 | def test_get_built_in_apps(self): 15 | """test get django built-in packages""" 16 | built_in_apps = [app.verbose_name for app in apps.get_app_configs() if app.name.startswith('django.')] 17 | read_me_apps = self.readme_generator.get_built_in_app_names() 18 | 19 | self.assertListEqual(read_me_apps, built_in_apps) 20 | 21 | def test_get_other_apps(self): 22 | """test get apps exclude built-in apps""" 23 | other_apps = [app.verbose_name for app in apps.get_app_configs() if not app.name.startswith('django.')] 24 | read_me_apps = self.readme_generator.get_installed_module_names() 25 | 26 | self.assertListEqual(read_me_apps, other_apps) 27 | 28 | def test_get_project_root_name(self): 29 | """project root dir name""" 30 | name = settings.BASE_DIR.name 31 | readme_name = self.readme_generator.get_project_name() 32 | 33 | self.assertEqual(name, readme_name) 34 | 35 | def test_has_docker_support(self): 36 | """check for docker-compose existence""" 37 | compose_file_yml = Path(f'{settings.BASE_DIR}/docker-compose.yml') 38 | compose_file_yaml = Path(f'{settings.BASE_DIR}/docker-compose.yaml') 39 | result = True if compose_file_yml.is_file() or compose_file_yaml.is_file() else False 40 | 41 | readme_result = self.readme_generator.has_docker_support() 42 | 43 | self.assertEqual(result, readme_result) 44 | -------------------------------------------------------------------------------- /sage_painless/utils/report_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - Report Generate Request Class 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | import json 7 | import os 8 | from pathlib import Path 9 | 10 | from django.conf import settings 11 | 12 | 13 | class ReportUserAnswer: 14 | def __init__(self, app_name, file_prefix): 15 | self.file_prefix = file_prefix 16 | self.app_label = app_name 17 | 18 | @classmethod 19 | def create_dir_is_not_exists(cls, directory): 20 | if not os.path.exists(directory): 21 | os.mkdir(directory) 22 | 23 | @classmethod 24 | def create_file_is_not_exists(cls, file_path): 25 | if not os.path.isfile(file_path): 26 | file = Path(file_path) 27 | file.touch(exist_ok=True) 28 | 29 | def init_report_file(self): 30 | """create report file in docs dir""" 31 | self.create_dir_is_not_exists(f'{settings.BASE_DIR}/docs/') 32 | self.create_dir_is_not_exists(f'{settings.BASE_DIR}/docs/sage_painless/') 33 | self.create_dir_is_not_exists(f'{settings.BASE_DIR}/docs/sage_painless/report/') 34 | self.create_file_is_not_exists( 35 | f'{settings.BASE_DIR}/docs/sage_painless/report/{self.file_prefix}-generation-report.json' 36 | ) 37 | 38 | def add_question_answer(self, question, answer): 39 | """append question answer to report file""" 40 | with open(f'{settings.BASE_DIR}/docs/sage_painless/report/{self.file_prefix}-generation-report.json', 'r+') as file: 41 | try: 42 | data = json.load(file) 43 | except: 44 | data = {} 45 | report = { 46 | question: answer 47 | } 48 | data.update(report) 49 | file.seek(0) 50 | json.dump(data, file) 51 | -------------------------------------------------------------------------------- /sage_painless/templates/test_model.jinja: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated Model test 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | from django.apps import apps 6 | from rest_framework.test import APITestCase 7 | from django_seed import Seed 8 | 9 | {% for model in models %} 10 | from {{app_name}}.models.{{model.name.lower()}} import {{model.name}} 11 | {% endfor %} 12 | 13 | seeder = Seed.seeder() 14 | 15 | class {{app_name.capitalize()}}ModelTest(APITestCase): 16 | """ 17 | {{app_name}} Model Test 18 | Auto Generated 19 | """ 20 | def setUp(self) -> None: 21 | self.models = apps.get_app_config('{{app_name}}').get_models() 22 | self.models_name = [model._meta.object_name for model in self.models] 23 | {% for model in models %} 24 | {% if not model.has_one_to_one() %} 25 | def test_{{model.name.lower()}}_model(self): 26 | """ 27 | test {{model.name}} creation 28 | """ 29 | seeder.add_entity({{model.name}}, 1) 30 | seeder.execute() # create instance 31 | # assertions 32 | self.assertTrue({{model.name}}.objects.exists()) 33 | self.assertIn('{{model.name}}', self.models_name) 34 | {% endif %} 35 | {% endfor %} 36 | {% for signal in signals %} 37 | def test_{{signal.model_a.lower()}}_model_signal(self): 38 | """ 39 | test {{signal.model_b}} - {{signal.model_a}} signal 40 | """ 41 | seeder.add_entity({{signal.model_b}}, 1) 42 | seeder.execute() # create instance 43 | # assertions 44 | self.assertTrue({{signal.model_b}}.objects.exists()) 45 | self.assertTrue({{signal.model_a}}.objects.exists()) 46 | first_object = {{signal.model_b}}.objects.first() 47 | second_object = {{signal.model_a}}.objects.first() 48 | self.assertEqual(second_object.{{signal.field}}, first_object) 49 | {%endfor%} 50 | -------------------------------------------------------------------------------- /sage_painless/services/nginx_generator.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - Nginx Config Generator 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | import os 7 | import time 8 | 9 | from django.conf import settings 10 | 11 | # Base 12 | from sage_painless.services.abstract import AbstractNiginxGenerator 13 | 14 | # Helpers 15 | from sage_painless.utils.git_service import GitSupport 16 | from sage_painless.utils.jinja_service import JinjaHandler 17 | from sage_painless.utils.timing_service import TimingService 18 | 19 | from sage_painless import templates 20 | 21 | 22 | class NginxGenerator( 23 | AbstractNiginxGenerator, JinjaHandler, TimingService, GitSupport 24 | ): 25 | NGINX_TEMPLATE = 'nginx.jinja' 26 | 27 | def __init__(self, *args, **kwargs): 28 | super().__init__(*args, **kwargs) 29 | 30 | def generate(self, git_support=False): 31 | """generate nginx.conf 32 | template: 33 | sage_painless/templates/nginx.jinja 34 | """ 35 | start_time = time.time() 36 | 37 | if git_support: 38 | self.init_repo(settings.BASE_DIR) 39 | 40 | # generate nginx.conf 41 | self.stream_to_template( 42 | output_path=f'{settings.BASE_DIR}/nginx.conf', 43 | template_path=os.path.abspath(templates.__file__).replace('__init__.py', self.NGINX_TEMPLATE), 44 | data={ 45 | 'static_files': self.get_static_files_dir(), 46 | 'media_files': self.get_media_files_dir() 47 | } 48 | ) 49 | 50 | if git_support: 51 | self.commit_file( 52 | f'{settings.BASE_DIR}/nginx.conf', 53 | f'deploy (nginx): Create nginx config file' 54 | ) 55 | 56 | end_time = time.time() 57 | return True, 'nginx config generated ({:.3f} ms)'.format(self.calculate_execute_time(start_time, end_time)) 58 | -------------------------------------------------------------------------------- /sage_painless/docs/diagrams/article_diagram.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": { 3 | "articles": { 4 | "models": { 5 | "Article": { 6 | "fields": { 7 | "title": { 8 | "type": "character", 9 | "max_length": 120 10 | }, 11 | "body": { 12 | "type": "character", 13 | "max_length": 255 14 | }, 15 | "slug": { 16 | "type": "slug", 17 | "max_length": 255, 18 | "unique": true 19 | }, 20 | "created": { 21 | "type": "datetime", 22 | "auto_now_add": true 23 | }, 24 | "publish": { 25 | "type": "datetime", 26 | "null": true, 27 | "blank": true 28 | }, 29 | "updated": { 30 | "type": "datetime", 31 | "auto_now": true 32 | }, 33 | "options": { 34 | "type": "character", 35 | "max_length": 2, 36 | "choices": [ 37 | [ 38 | "dr", 39 | "Draft" 40 | ], 41 | [ 42 | "pb", 43 | "public" 44 | ], 45 | [ 46 | "sn", 47 | "soon" 48 | ] 49 | ] 50 | } 51 | }, 52 | "admin": { 53 | "list_display": [ 54 | "title", 55 | "created", 56 | "updated" 57 | ], 58 | "list_filter": [ 59 | "created", 60 | "updated", 61 | "options" 62 | ], 63 | "search_fields": [ 64 | "title", 65 | "body" 66 | ] 67 | }, 68 | "api": { 69 | "methods": [ 70 | "get", 71 | "post" 72 | ] 73 | } 74 | } 75 | } 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /products/admin.py: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated admin.py 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | from django.contrib import admin 6 | 7 | from products.models.category import Category 8 | 9 | from products.models.product import Product 10 | 11 | from products.models.discount import Discount 12 | 13 | 14 | @admin.register(Category) 15 | class CategoryAdmin(admin.ModelAdmin): 16 | """ 17 | Category Admin 18 | Auto generated 19 | """ 20 | list_display = ['title', 'created', 'modified'] 21 | 22 | list_filter = ['created', 'modified'] 23 | 24 | search_fields = ['title'] 25 | 26 | def has_add_permission(self, *args, **kwargs): 27 | return True 28 | 29 | def has_change_permission(self, *args, **kwargs): 30 | return True 31 | 32 | def has_delete_permission(self, *args, **kwargs): 33 | return True 34 | 35 | 36 | @admin.register(Product) 37 | class ProductAdmin(admin.ModelAdmin): 38 | """ 39 | Product Admin 40 | Auto generated 41 | """ 42 | list_display = ['title', 'price', 'category'] 43 | 44 | list_filter = ['created', 'modified'] 45 | 46 | search_fields = ['title', 'description'] 47 | 48 | raw_id_fields = ['category'] 49 | 50 | def has_add_permission(self, *args, **kwargs): 51 | return True 52 | 53 | def has_change_permission(self, *args, **kwargs): 54 | return True 55 | 56 | def has_delete_permission(self, *args, **kwargs): 57 | return True 58 | 59 | 60 | @admin.register(Discount) 61 | class DiscountAdmin(admin.ModelAdmin): 62 | """ 63 | Discount Admin 64 | Auto generated 65 | """ 66 | list_display = ['discount', 'product', 'created', 'modified'] 67 | 68 | list_filter = ['created', 'modified'] 69 | 70 | raw_id_fields = ['product'] 71 | 72 | def has_add_permission(self, *args, **kwargs): 73 | return True 74 | 75 | def has_change_permission(self, *args, **kwargs): 76 | return True 77 | 78 | def has_delete_permission(self, *args, **kwargs): 79 | return True 80 | -------------------------------------------------------------------------------- /sage_painless/services/uwsgi_generator.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - UWSGI Config Generator 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | import os 7 | import time 8 | 9 | from django.conf import settings 10 | 11 | # Base 12 | from sage_painless.services.abstract import AbstractUWSGIGenerator 13 | 14 | # Helpers 15 | from sage_painless.utils.git_service import GitSupport 16 | from sage_painless.utils.jinja_service import JinjaHandler 17 | from sage_painless.utils.json_service import JsonHandler 18 | from sage_painless.utils.timing_service import TimingService 19 | 20 | from sage_painless import templates 21 | 22 | 23 | class UwsgiGenerator(AbstractUWSGIGenerator, JinjaHandler, JsonHandler, TimingService, GitSupport): 24 | """generate uwsgi config""" 25 | UWSGI_TEMPLATE = 'uwsgi.jinja' 26 | 27 | def __init__(self, *args, **kwargs): 28 | super().__init__(*args, **kwargs) 29 | 30 | def generate(self, diagram_path, git_support=False): 31 | """generate uwsgi.ini 32 | template: 33 | sage_painless/templates/uwsgi.jinja 34 | """ 35 | start_time = time.time() 36 | 37 | diagram = self.load_json(diagram_path) 38 | 39 | config = self.extract_uwsgi_config(diagram) # get uwsgi config from diagram 40 | 41 | if git_support: 42 | self.init_repo(settings.BASE_DIR) 43 | 44 | # generate uwsgi.ini 45 | self.stream_to_template( 46 | output_path=f'{settings.BASE_DIR}/uwsgi.ini', 47 | template_path=os.path.abspath(templates.__file__).replace('__init__.py', self.UWSGI_TEMPLATE), 48 | data={ 49 | 'config': config 50 | } 51 | ) 52 | 53 | if git_support: 54 | self.commit_file( 55 | f'{settings.BASE_DIR}/uwsgi.ini', 56 | f'deploy (uwsgi): Create uwsgi config file' 57 | ) 58 | 59 | end_time = time.time() 60 | return True, 'uwsgi config generated ({:.3f} ms)'.format(self.calculate_execute_time(start_time, end_time)) 61 | -------------------------------------------------------------------------------- /sage_painless/management/commands/docs.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - docs management command 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | import datetime 7 | 8 | from django.core.management import BaseCommand 9 | 10 | from sage_painless.services.readme_generator import ReadMeGenerator 11 | from sage_painless.utils.report_service import ReportUserAnswer 12 | 13 | 14 | class Command(BaseCommand): 15 | help = 'Generate all docs.' 16 | 17 | def add_arguments(self, parser): 18 | """initialize arguments""" 19 | parser.add_argument( 20 | '-d', '--diagram', type=str, help='sql diagram path that will make models.py from it', required=True) 21 | parser.add_argument('-g', '--git', type=bool, help='generate git commits', required=False) 22 | 23 | def handle(self, *args, **options): 24 | diagram_path = options.get('diagram') 25 | git_support = options.get('git', False) 26 | stdout_messages = list() 27 | docs_support = input('Would you like to generate README.md(yes/no)? ') 28 | 29 | reporter = ReportUserAnswer( 30 | app_name='docs', 31 | file_prefix=f'docs-{int(datetime.datetime.now().timestamp())}' 32 | ) 33 | reporter.init_report_file() 34 | 35 | if docs_support == 'yes': 36 | reporter.add_question_answer( 37 | question='create README', 38 | answer=True 39 | ) 40 | readme_generator = ReadMeGenerator() 41 | check, message = readme_generator.generate(diagram_path, git_support=git_support) 42 | if check: 43 | stdout_messages.append(self.style.SUCCESS(f'docs[INFO]: {message}')) 44 | else: 45 | stdout_messages.append(self.style.ERROR(f'docs[ERROR]: {message}')) 46 | else: 47 | reporter.add_question_answer( 48 | question='create README', 49 | answer=False 50 | ) 51 | 52 | for message in stdout_messages: 53 | self.stdout.write(message) 54 | -------------------------------------------------------------------------------- /sage_painless/utils/package_manager_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - Package Manager Integration Main Class 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | import os 7 | import subprocess 8 | 9 | from django.conf import settings 10 | 11 | 12 | class PackageManagerSupport: 13 | VALID_MANAGERS = ['pip', 'pipenv', 'Pipenv', 'poetry'] 14 | COMMANDS = { 15 | 'pip': ['pip freeze > requirements.txt'], 16 | 'pipenv': ['pipenv lock -r > requirements.txt'], 17 | 'Pipenv': ['pipenv lock -r > requirements.txt'], 18 | 'poetry': ['poetry export -f requirements.txt --output requirements.txt'] 19 | } 20 | 21 | def __init__(self, *args, **kwargs): 22 | super().__init__(*args, **kwargs) 23 | self.manager = None 24 | 25 | def validate_package_manager_type(self, _type): 26 | """check manager type is valid""" 27 | if _type not in self.VALID_MANAGERS: 28 | raise KeyError(f'package manager {_type} is not supported') 29 | 30 | def check_manager_is_ready(self): 31 | """check self.manager is set""" 32 | if not self.manager: 33 | raise NotImplementedError('self.manager should set first') 34 | 35 | def set_package_manager_type(self, _type): 36 | """set self.manager""" 37 | self.validate_package_manager_type(_type) 38 | self.manager = _type 39 | return self.manager 40 | 41 | @classmethod 42 | def run_requirements_command(cls, manager): 43 | """get export req command for each package manager 44 | call using subprocess 45 | """ 46 | command = cls.COMMANDS.get(manager) 47 | if command: 48 | os.chdir(settings.BASE_DIR) # go to base dir 49 | subprocess.call(command, shell=True) # export packages 50 | else: 51 | SystemError('command not found for exporting requirements') 52 | 53 | def export_requirements(self): 54 | """export requirement packages""" 55 | self.check_manager_is_ready() 56 | self.run_requirements_command(self.manager) 57 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'django-sage-painless' 21 | copyright = '2021, SageTeam' 22 | author = 'SageTeam' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = 'version 0.0.1' 26 | 27 | # -- General configuration --------------------------------------------------- 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | ] 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ['_templates'] 37 | 38 | # List of patterns, relative to source directory, that match files and 39 | # directories to ignore when looking for source files. 40 | # This pattern also affects html_static_path and html_extra_path. 41 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 42 | 43 | # -- Options for HTML output ------------------------------------------------- 44 | 45 | # The theme to use for HTML and HTML Help pages. See the documentation for 46 | # a list of builtin themes. 47 | # 48 | html_theme = "sphinx_rtd_theme" 49 | 50 | # Add any paths that contain custom static files (such as style sheets) here, 51 | # relative to this directory. They are copied after the builtin static files, 52 | # so a file named "default.css" will overwrite the builtin "default.css". 53 | html_static_path = ['_static'] 54 | -------------------------------------------------------------------------------- /sage_painless/templates/models.jinja: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated models.py 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | from django.db import models 6 | from django.utils.translation import ugettext_lazy as _ 7 | {% if validator_support %} 8 | # validator support 9 | from importlib import import_module 10 | validators = import_module('django.core.validators') 11 | {% endif %} 12 | {% if cache_support %} 13 | # cache support 14 | from {{app_name}}.mixins import ModelCacheMixin 15 | {% endif %} 16 | {% if encrypt_support %} 17 | # encrypt support 18 | from sage_encrypt.services.encrypt import encrypt_field 19 | {% endif %} 20 | {% if fk_models %} 21 | {% for model in fk_models %} 22 | from {{app_name}}.models.{{model.lower()}} import {{model}} 23 | {% endfor %} 24 | {% endif %} 25 | {% for model in models %} 26 | 27 | class {{model.name}}(models.Model{% if cache_support %}, ModelCacheMixin{% endif %}): 28 | """ 29 | {{model.name}} Model 30 | Auto generated 31 | """ 32 | {% if cache_support %} 33 | CACHE_KEY = '{{model.name.lower()}}' # auto generated CACHE_KEY 34 | {% endif %} 35 | {% for field in model.fields %} 36 | {% if field.type %} 37 | {{field.name}} = {% if field.encrypted %}encrypt_field({% endif %}models.{{field.type}}({% for attr in field.attrs %} 38 | {% if attr.key == 'on_delete' %}{{attr.key}}=models.{{attr.value}},{% else %}{{attr.key}}={{attr.value}},{%endif%}{% endfor %} 39 | {% if field.validators %} 40 | validators=[ 41 | {% for validator in field.validators %} 42 | getattr(validators, '{{validator.func}}')({{validator.arg}}), 43 | {% endfor %} 44 | ] 45 | {% endif %} 46 | ){% if field.encrypted %}){% endif %} 47 | {% endif %} 48 | {% endfor %} 49 | 50 | def __str__(self): 51 | return {% if model.get_str %} f"{{model.get_str}} {self.pk}" {% else %} f"{self.pk}" {% endif %} 52 | 53 | class Meta: 54 | verbose_name = _("{{model.verbose_name}}") # auto generated verbose_name 55 | verbose_name_plural = _("{{model.verbose_name_plural}}") 56 | {% endfor %} -------------------------------------------------------------------------------- /sage_painless/templates/test_api.jinja: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated API test 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | from django.urls import reverse 6 | from rest_framework.test import APITestCase 7 | from django_seed import Seed 8 | 9 | {% for model in models %} 10 | from {{app_name}}.models.{{model.name.lower()}} import {{model.name}} 11 | {% endfor %} 12 | 13 | 14 | seeder = Seed.seeder() 15 | 16 | class {{app_name.capitalize()}}APITest(APITestCase): 17 | """ 18 | {{app_name}} API Test 19 | Auto Generated 20 | """ 21 | def setUp(self) -> None: 22 | {% for model in models %} 23 | {% if not model.has_one_to_one() %} 24 | seeder.add_entity({{model.name}}, 3) 25 | {% endif %} 26 | {% endfor %} 27 | seeder.execute() # create instances 28 | {% for model in models %} 29 | def test_{{model.name.lower()}}_list_success(self): 30 | """ 31 | test {{model.name}} list 32 | """ 33 | url = reverse('{{model.name.lower()}}-list') 34 | response = self.client.get(url) 35 | # assertions 36 | self.assertEqual(response.status_code, 200) 37 | if type(response.data) == dict: 38 | if response.data.get('count'): 39 | self.assertGreater(response.data['count'], 0) 40 | else: 41 | self.assertGreater(len(response.data), 0) 42 | 43 | def test_{{model.name.lower()}}_detail_success(self): 44 | """ 45 | test {{model.name}} detail 46 | """ 47 | {{model.name.lower()}} = {{model.name}}.objects.first() 48 | url = reverse('{{model.name.lower()}}-detail', args=[{{model.name.lower()}}.pk]) 49 | response = self.client.get(url) 50 | # assertions 51 | self.assertEqual(response.status_code, 200) 52 | {% for field in model.fields %} 53 | {% if field.type != 'DateTimeField' and field.type != 'DateField' and field.type != 'ForeignKey' and field.type != 'OneToOneField' and field.type != 'ManyToMany' and field.type != 'ImageField' %} 54 | self.assertEqual(response.data.get('{{field.name}}'), {{model.name.lower()}}.{{field.name}}) 55 | {% endif %} 56 | {% endfor %} 57 | {% endfor %} 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | .idea/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | 103 | media/upload/ 104 | # mypy 105 | .mypy_cache/ 106 | settings.ini 107 | .vscode 108 | 109 | collectstatic/ 110 | CACHE/ 111 | **/migrations/** 112 | !**/migrations 113 | !**/migrations/__init__.py 114 | 115 | .vscode/* 116 | 117 | docker-compose.yml 118 | Dockerfile 119 | 120 | jwt-key 121 | jwt-key.pub 122 | 123 | .docker 124 | 125 | kernel/ 126 | manage.py 127 | 128 | docs/sage_painless/ 129 | 130 | coverage_html_report/ 131 | 132 | psude.py 133 | sage_painless/services/model_generator.py_pseudo.txt 134 | 135 | fifo.py 136 | gunicorn-conf.py 137 | uwsgi.ini 138 | .env.prod 139 | nginx.conf -------------------------------------------------------------------------------- /sage_painless/classes/model.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - Model Class 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | 7 | 8 | class Model: 9 | """ 10 | Model object that contains `name` and `fields` of model 11 | """ 12 | 13 | name = None 14 | fields = [] 15 | api_config = {} 16 | 17 | def has_one_to_one(self): 18 | """ 19 | model has one2one field 20 | """ 21 | for field in self.fields: 22 | if field.type == 'OneToOneField': 23 | return True 24 | 25 | return False 26 | 27 | @property 28 | def filter_fields(self): 29 | """return model filter patterns""" 30 | fields = list() 31 | for field in self.fields: 32 | if field.type in ['ForeignKey', 'OneToOneField', 'ManyToManyField']: 33 | fields.append(f'{field.name}__id') 34 | else: 35 | fields.append(field.name) 36 | return fields 37 | 38 | @property 39 | def search_fields(self): 40 | """return model search field patterns""" 41 | fields = list() 42 | for field in self.fields: 43 | if field.type in ['CharField', 'TextField', 'DateTimeField', 'DateField', 'TimeField', 'SlugField']: 44 | fields.append(field.name) 45 | return fields 46 | 47 | @property 48 | def verbose_name(self): 49 | """ 50 | create model `verbose_name` 51 | """ 52 | return self.name.capitalize() if self.name else 'Not Defined' 53 | 54 | @property 55 | def verbose_name_plural(self): 56 | """ 57 | create model `verbose_name_plural` 58 | """ 59 | if self.name: 60 | wordlist = [] 61 | for char in self.name: 62 | wordlist.append(char) 63 | if self.name[len(self.name) - 1] == "y": 64 | wordlist[len(self.name) - 1] = "ies" 65 | else: 66 | wordlist.append("s") 67 | word = "" 68 | for i in wordlist: 69 | word += i 70 | return word 71 | else: 72 | return 'Not Defined' 73 | 74 | @property 75 | def get_str(self): 76 | """ 77 | return `__str__` value for model 78 | """ 79 | return self.name 80 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | BASE_DIR = Path(__file__).resolve().parent.parent 4 | 5 | SECRET_KEY = 'most-secret-key-for-test' 6 | 7 | DEBUG = True 8 | 9 | INSTALLED_APPS = [ 10 | 'django.contrib.admin', 11 | 'django.contrib.auth', 12 | 'django.contrib.contenttypes', 13 | 'django.contrib.sessions', 14 | 'django.contrib.messages', 15 | 'django.contrib.staticfiles', 16 | 'sage_painless', 17 | 'rest_framework' 18 | ] 19 | 20 | MIDDLEWARE = [ 21 | 'django.middleware.security.SecurityMiddleware', 22 | 'django.contrib.sessions.middleware.SessionMiddleware', 23 | 'django.middleware.common.CommonMiddleware', 24 | 'django.middleware.csrf.CsrfViewMiddleware', 25 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 26 | 'django.contrib.messages.middleware.MessageMiddleware', 27 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 28 | ] 29 | 30 | TEMPLATES = [ 31 | { 32 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 33 | 'DIRS': [], 34 | 'APP_DIRS': True, 35 | 'OPTIONS': { 36 | 'context_processors': [ 37 | 'django.template.context_processors.debug', 38 | 'django.template.context_processors.request', 39 | 'django.contrib.auth.context_processors.auth', 40 | 'django.contrib.messages.context_processors.messages', 41 | ], 42 | }, 43 | }, 44 | ] 45 | 46 | DATABASES = { 47 | 'default': { 48 | 'ENGINE': 'django.db.backends.sqlite3', 49 | 'NAME': BASE_DIR / 'db.sqlite3', 50 | } 51 | } 52 | 53 | AUTH_PASSWORD_VALIDATORS = [ 54 | { 55 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 56 | }, 57 | { 58 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 59 | }, 60 | { 61 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 62 | }, 63 | { 64 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 65 | }, 66 | ] 67 | 68 | LANGUAGE_CODE = 'en-us' 69 | 70 | TIME_ZONE = 'UTC' 71 | 72 | USE_I18N = True 73 | 74 | USE_L10N = True 75 | 76 | USE_TZ = True 77 | 78 | STATIC_URL = '/static/' 79 | 80 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 81 | -------------------------------------------------------------------------------- /products/tests/test_category.py: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated test 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | from django.apps import apps 6 | from django.urls import reverse 7 | from rest_framework.test import APITestCase 8 | from django_seed import Seed 9 | 10 | 11 | from products.models.category import Category 12 | 13 | from products.models.product import Product 14 | 15 | from products.models.discount import Discount 16 | 17 | 18 | seeder = Seed.seeder() 19 | 20 | 21 | class CategoryTest(APITestCase): 22 | """ 23 | Category Test 24 | Auto Generated 25 | """ 26 | def setUp(self) -> None: 27 | self.models = apps.get_app_config('products').get_models() 28 | self.models_name = [model._meta.object_name for model in self.models] 29 | 30 | seeder.add_entity(Category, 3) 31 | 32 | seeder.add_entity(Product, 3) 33 | 34 | seeder.add_entity(Discount, 3) 35 | 36 | seeder.execute() # create instances 37 | 38 | def test_category_model(self): 39 | """test Category creation""" 40 | seeder.add_entity(Category, 1) 41 | seeder.execute() # create instance 42 | # assertions 43 | self.assertTrue(Category.objects.exists()) 44 | self.assertIn('Category', self.models_name) 45 | 46 | def test_category_list_success(self): 47 | """test Category list""" 48 | url = reverse('category-list') 49 | response = self.client.get(url) 50 | # assertions 51 | self.assertEqual(response.status_code, 200) 52 | if type(response.data) == dict: 53 | if response.data.get('count'): 54 | self.assertGreater(response.data['count'], 0) 55 | else: 56 | self.assertGreater(len(response.data), 0) 57 | 58 | def test_category_detail_success(self): 59 | """test Category detail""" 60 | category = Category.objects.first() 61 | url = reverse('category-detail', args=[category.pk]) 62 | response = self.client.get(url) 63 | # assertions 64 | self.assertEqual(response.status_code, 200) 65 | 66 | self.assertEqual(response.data.get('title'), category.title) 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /products/tests/test_discount.py: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated test 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | from django.apps import apps 6 | from django.urls import reverse 7 | from rest_framework.test import APITestCase 8 | from django_seed import Seed 9 | 10 | 11 | from products.models.category import Category 12 | 13 | from products.models.product import Product 14 | 15 | from products.models.discount import Discount 16 | 17 | 18 | seeder = Seed.seeder() 19 | 20 | 21 | class DiscountTest(APITestCase): 22 | """ 23 | Discount Test 24 | Auto Generated 25 | """ 26 | def setUp(self) -> None: 27 | self.models = apps.get_app_config('products').get_models() 28 | self.models_name = [model._meta.object_name for model in self.models] 29 | 30 | seeder.add_entity(Category, 3) 31 | 32 | seeder.add_entity(Product, 3) 33 | 34 | seeder.add_entity(Discount, 3) 35 | 36 | seeder.execute() # create instances 37 | 38 | def test_discount_model(self): 39 | """test Discount creation""" 40 | seeder.add_entity(Discount, 1) 41 | seeder.execute() # create instance 42 | # assertions 43 | self.assertTrue(Discount.objects.exists()) 44 | self.assertIn('Discount', self.models_name) 45 | 46 | def test_discount_list_success(self): 47 | """test Discount list""" 48 | url = reverse('discount-list') 49 | response = self.client.get(url) 50 | # assertions 51 | self.assertEqual(response.status_code, 200) 52 | if type(response.data) == dict: 53 | if response.data.get('count'): 54 | self.assertGreater(response.data['count'], 0) 55 | else: 56 | self.assertGreater(len(response.data), 0) 57 | 58 | def test_discount_detail_success(self): 59 | """test Discount detail""" 60 | discount = Discount.objects.first() 61 | url = reverse('discount-detail', args=[discount.pk]) 62 | response = self.client.get(url) 63 | # assertions 64 | self.assertEqual(response.status_code, 200) 65 | 66 | self.assertEqual(response.data.get('discount'), discount.discount) 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /sage_painless/services/gunicorn_generator.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - Gunicorn Generator 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | import os 7 | import time 8 | 9 | from django.conf import settings 10 | 11 | # Base 12 | from sage_painless.services.abstract import AbstractGunicornGenerator 13 | 14 | # Helpers 15 | from sage_painless.utils.comment_service import CommentService 16 | from sage_painless.utils.file_service import FileService 17 | from sage_painless.utils.git_service import GitSupport 18 | from sage_painless.utils.jinja_service import JinjaHandler 19 | from sage_painless.utils.json_service import JsonHandler 20 | from sage_painless.utils.pep8_service import Pep8 21 | from sage_painless.utils.timing_service import TimingService 22 | 23 | from sage_painless import templates 24 | 25 | 26 | class GunicornGenerator( 27 | AbstractGunicornGenerator, JinjaHandler, JsonHandler, Pep8, FileService, CommentService, TimingService, GitSupport 28 | ): 29 | """gunicorn config generator""" 30 | CONF_TEMPLATE = 'conf.jinja' 31 | 32 | def __init__(self, *args, **kwargs): 33 | super().__init__(*args, **kwargs) 34 | 35 | def generate(self, diagram_path, git_support=False): 36 | """generate conf.py 37 | template: 38 | sage_painless/templates/conf.jinja 39 | """ 40 | start_time = time.time() 41 | 42 | diagram = self.load_json(diagram_path) 43 | 44 | if git_support: 45 | self.init_repo(settings.BASE_DIR) 46 | 47 | config = self.extract_gunicorn_config(diagram) # get gunicorn config from diagram 48 | 49 | # generate conf.py 50 | self.stream_to_template( 51 | output_path=f'{settings.BASE_DIR}/gunicorn-conf.py', 52 | template_path=os.path.abspath(templates.__file__).replace('__init__.py', self.CONF_TEMPLATE), 53 | data={ 54 | 'config': config, 55 | 'comments': self.GUNICORN_CONFIG_COMMENTS 56 | } 57 | ) 58 | self.fix_pep8(f'{settings.BASE_DIR}/gunicorn-conf.py') 59 | if git_support: 60 | self.commit_file( 61 | f'{settings.BASE_DIR}/gunicorn-conf.py', 62 | f'deploy (gunicorn): Create gunicorn config file' 63 | ) 64 | 65 | end_time = time.time() 66 | return True, 'gunicorn config generated ({:.3f} ms)'.format(self.calculate_execute_time(start_time, end_time)) 67 | -------------------------------------------------------------------------------- /products/tests/test_product.py: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated test 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | from django.apps import apps 6 | from django.urls import reverse 7 | from rest_framework.test import APITestCase 8 | from django_seed import Seed 9 | 10 | 11 | from products.models.category import Category 12 | 13 | from products.models.product import Product 14 | 15 | from products.models.discount import Discount 16 | 17 | 18 | seeder = Seed.seeder() 19 | 20 | 21 | class ProductTest(APITestCase): 22 | """ 23 | Product Test 24 | Auto Generated 25 | """ 26 | def setUp(self) -> None: 27 | self.models = apps.get_app_config('products').get_models() 28 | self.models_name = [model._meta.object_name for model in self.models] 29 | 30 | seeder.add_entity(Category, 3) 31 | 32 | seeder.add_entity(Product, 3) 33 | 34 | seeder.add_entity(Discount, 3) 35 | 36 | seeder.execute() # create instances 37 | 38 | def test_product_model(self): 39 | """test Product creation""" 40 | seeder.add_entity(Product, 1) 41 | seeder.execute() # create instance 42 | # assertions 43 | self.assertTrue(Product.objects.exists()) 44 | self.assertIn('Product', self.models_name) 45 | 46 | def test_product_list_success(self): 47 | """test Product list""" 48 | url = reverse('product-list') 49 | response = self.client.get(url) 50 | # assertions 51 | self.assertEqual(response.status_code, 200) 52 | if type(response.data) == dict: 53 | if response.data.get('count'): 54 | self.assertGreater(response.data['count'], 0) 55 | else: 56 | self.assertGreater(len(response.data), 0) 57 | 58 | def test_product_detail_success(self): 59 | """test Product detail""" 60 | product = Product.objects.first() 61 | url = reverse('product-detail', args=[product.pk]) 62 | response = self.client.get(url) 63 | # assertions 64 | self.assertEqual(response.status_code, 200) 65 | 66 | self.assertEqual(response.data.get('title'), product.title) 67 | 68 | self.assertEqual(response.data.get('description'), product.description) 69 | 70 | self.assertEqual(response.data.get('price'), product.price) 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /sage_painless/services/admin_generator.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - Admin Generator 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | import os 7 | import time 8 | 9 | from django.conf import settings 10 | 11 | # Base 12 | from sage_painless.services.abstract import AbstractAdminGenerator 13 | 14 | # Helpers 15 | from sage_painless.utils.file_service import FileService 16 | from sage_painless.utils.git_service import GitSupport 17 | from sage_painless.utils.jinja_service import JinjaHandler 18 | from sage_painless.utils.json_service import JsonHandler 19 | from sage_painless.utils.pep8_service import Pep8 20 | from sage_painless.utils.timing_service import TimingService 21 | 22 | from sage_painless import templates 23 | 24 | 25 | class AdminGenerator(AbstractAdminGenerator, JinjaHandler, JsonHandler, Pep8, FileService, TimingService, GitSupport): 26 | """Generate admin.py for apps from given diagram""" 27 | ADMIN_TEMPLATE = 'admin.jinja' 28 | 29 | def __init__(self, *args, **kwargs): 30 | """init""" 31 | super().__init__(*args, **kwargs) 32 | 33 | def generate(self, diagram_path: str, app_name: str, git_support: bool = False): 34 | """generate admin.py 35 | template: 36 | sage_painless/templates/admin.jinja 37 | """ 38 | start_time = time.time() 39 | diagram = self.load_json(diagram_path) 40 | if git_support: 41 | self.init_repo(settings.BASE_DIR) 42 | models_diagram = diagram.get( 43 | self.get_constant('APPS_KEYWORD')).get(app_name).get( 44 | self.get_constant('MODELS_KEYWORD')) # get models data for current app 45 | admins = self.extract_admin(models_diagram) 46 | 47 | self.create_app_if_not_exists(app_name) 48 | 49 | self.stream_to_template( 50 | output_path=f'{settings.BASE_DIR}/{app_name}/admin.py', 51 | template_path=os.path.abspath(templates.__file__).replace('__init__.py', self.ADMIN_TEMPLATE), 52 | data={ 53 | 'app_name': app_name, 54 | 'admins': admins, 55 | } 56 | ) 57 | self.fix_pep8(f'{settings.BASE_DIR}/{app_name}/admin.py') 58 | if git_support: 59 | self.commit_file( 60 | f'{settings.BASE_DIR}/{app_name}/admin.py', 61 | f'feat ({app_name}--admin): Add models to admin.py' 62 | ) 63 | end_time = time.time() 64 | return True, 'admin generated ({:.3f} ms)'.format(self.calculate_execute_time(start_time, end_time)) 65 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. django-sage-painless documentation master file, created by 2 | sphinx-quickstart on Mon Jun 14 13:15:46 2021. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to django-sage-painless's documentation! 7 | ================================================ 8 | 9 | .. |br| raw:: html 10 | 11 |
12 | 13 | .. image:: https://github.com/sageteam-org/django-sage-painless/blob/develop/docs/images/tag_sage.png?raw=true 14 | :target: https://sageteam.org/ 15 | :alt: SageTeam 16 | 17 | |br| 18 | 19 | .. image:: https://img.shields.io/pypi/v/django-sage-painless 20 | :target: https://pypi.org/project/django-sage-painless/ 21 | :alt: PyPI release 22 | 23 | .. image:: https://img.shields.io/pypi/pyversions/django-sage-painless 24 | :target: https://pypi.org/project/django-sage-painless/ 25 | :alt: Supported Python versions 26 | 27 | .. image:: https://img.shields.io/pypi/djversions/django-sage-painless 28 | :target: https://pypi.org/project/django-sage-painless/ 29 | :alt: Supported Django versions 30 | 31 | .. image:: https://img.shields.io/readthedocs/django-sage-painless 32 | :target: https://django-sage-painless.readthedocs.io/ 33 | :alt: Documentation 34 | 35 | .. image:: https://img.shields.io/appveyor/build/mrhnz/django-sage-painless 36 | :target: https://django-sage-painless.readthedocs.io/ 37 | :alt: Build 38 | 39 | |br| 40 | 41 | This app supports the following combinations of Django and Python: 42 | 43 | ========== ======================= 44 | Django Python 45 | ========== ======================= 46 | 3.1 3.7, 3.8, 3.9 47 | 3.2 3.7, 3.8, 3.9 48 | ========== ======================= 49 | 50 | 51 | Functionality 52 | ------------- 53 | 54 | painless creates django backend projects without developer coding 55 | 56 | it can generate these parts: 57 | 58 | - models.py 59 | - signals.py 60 | - admin.py 61 | - serializers.py 62 | - views.py 63 | - urls.py 64 | - tests 65 | - API documentation 66 | - Dockerfile 67 | - docker-compose.yml 68 | - cache queryset (Redis) 69 | - video streaming 70 | - database encryption (PostgreSQL) 71 | - tox 72 | - coverage 73 | - gunicorn 74 | - uwsgi 75 | - README.md 76 | 77 | Documentation 78 | ------------- 79 | 80 | .. toctree:: 81 | :maxdepth: 3 82 | 83 | quick_start 84 | usage 85 | diagram 86 | contribute 87 | faq 88 | 89 | Issues 90 | ------ 91 | 92 | If you have questions or have trouble using the app please file a bug report at: 93 | 94 | https://github.com/sageteam-org/django-sage-painless/issues 95 | 96 | 97 | 98 | Indices and tables 99 | ================== 100 | 101 | * :ref:`search` 102 | -------------------------------------------------------------------------------- /sage_painless/templates/README.jinja: -------------------------------------------------------------------------------- 1 | # {{project_name}} 2 | 3 | This is an auto-generated README.md for `{{project_name}}` project 4 | 5 | - [Description](#description) 6 | - [Detail](#detail) 7 | - [Structure](#structure) 8 | - [Getting Started](#getting-started) 9 | 10 | ## Description 11 | 12 | - Write your own 13 | 14 | ## Detail 15 | 16 | - Version: {{project_version}} 17 | - Language: Python: {{django_version}} 18 | - Framework: Django: {{py_version}} 19 | 20 | Built-in Django Apps: 21 | 22 | {% for app in built_in_apps %} 23 | - {{app}} 24 | {% endfor %} 25 | 26 | Other Used Apps: 27 | 28 | {% for app in installed_modules %} 29 | - {{app}} 30 | {% endfor %} 31 | 32 | ## Structure 33 | 34 | {{project_name}} files structure 35 | 36 | ```shell 37 | {% for line in structure %} 38 | {{line}} 39 | {% endfor %} 40 | ``` 41 | 42 | ## Getting Started 43 | 44 | {% if docker_support %} 45 | 46 | ### Docker 47 | 48 | The project is dockerized for deployment. 49 | 50 | - Change directory to root of your project and run: 51 | 52 | ```shell 53 | $ docker-compose up -d --build 54 | ``` 55 | 56 | - Collect static files: 57 | 58 | ```shell 59 | $ docker-compose exec python manage.py collectstatic 60 | ``` 61 | 62 | - Test: 63 | 64 | ```shell 65 | $ docker-compose exec python manage.py check --deploy 66 | $ docker-compose exec python manage.py test 67 | ``` 68 | 69 | {% endif %} 70 | 71 | ### Django Server 72 | 73 | - Before running Django project you must first create virtualenv. 74 | 75 | ``` shell 76 | $ python{{py_version}} -m pip install virtualenv 77 | $ python{{py_version}} -m virtualenv venv 78 | ``` 79 | 80 | - To activate virtualenvironment: 81 | 82 | ubuntu: 83 | 84 | ```shell 85 | $ source venv/bin/activate 86 | ``` 87 | 88 | windows: 89 | 90 | ```shell 91 | $ venv\Scripts\activate.bat 92 | ``` 93 | 94 | - To deactivate vritualenvironment use: 95 | 96 | ``` shell 97 | $ deactivate 98 | ``` 99 | 100 | - After activation must install all packages: 101 | ```shell 102 | $ pip install -r requirements.txt 103 | ``` 104 | 105 | - Check your settings for db config and continue to create database: 106 | 107 | ``` sql 108 | CREATE USER WITH PASSWORD ; 109 | CREATE DATABASE ; 110 | ALTER ROLE SET client_encoding TO 'utf8'; 111 | ALTER ROLE SET default_transaction_isolation TO 'read committed'; 112 | ALTER ROLE SET timezone TO 'UTC'; 113 | GRANT ALL PRIVILEGES ON DATABASE TO ; 114 | ``` 115 | 116 | - Run check command to connect db: 117 | 118 | ```shell 119 | $ python manage.py check 120 | ``` 121 | 122 | - Create all database tables: 123 | 124 | ```shell 125 | $ python manage.py makemigrations 126 | $ python manage.py migrate 127 | ``` 128 | 129 | - On deployment: 130 | 131 | ```shell 132 | $ python manage.py collectstatic 133 | $ python manage.py test 134 | $ python manage.py check --deploy 135 | ``` 136 | 137 | #### Automatically generated with ❤️ by [django-sage-painless](https://github.com/sageteam-org/django-sage-painless) -------------------------------------------------------------------------------- /sage_painless/templates/views.jinja: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated views.py 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | {% if cache_support %} 6 | from django.http import Http404 7 | {% endif %} 8 | from rest_framework.viewsets import ModelViewSet 9 | {% if permission_support %} 10 | # permission support 11 | from rest_framework import permissions 12 | {% endif %} 13 | {% if filter_support %} 14 | # filter support 15 | from django_filters.rest_framework import DjangoFilterBackend 16 | {% endif %} 17 | {% if search_support %} 18 | # search support 19 | from rest_framework.filters import SearchFilter 20 | {% endif %} 21 | 22 | {% for model in models %} 23 | from {{app_name}}.models.{{model.name.lower()}} import {{model.name}} 24 | {% endfor %} 25 | from {{app_name}}.api.serializers import ( 26 | {% for model in models %}{{model.name}}Serializer, 27 | {% endfor %} 28 | ) 29 | 30 | {% for model in models %} 31 | class {{model.name}}Viewset(ModelViewSet): 32 | """ 33 | {{model.name}} Viewset 34 | Auto generated 35 | """ 36 | serializer_class = {{model.name}}Serializer 37 | {% if model.api_config %} 38 | {% if model.api_config.get('methods') %}http_method_names = {{model.api_config.get('methods')}}{% endif %} 39 | {% if model.api_config.get('permission') %}permission_classes = (permissions.{{model.api_config.get('permission')}},){% endif %} 40 | {% if model.api_config.get('filter') and model.api_config.get('search') %} 41 | filter_backends = [DjangoFilterBackend, SearchFilter] 42 | search_fields = [ 43 | {% for field in model.search_fields %}'{{field}}',{% endfor %} 44 | ] 45 | filterset_fields = [ 46 | {% for field in model.filter_fields %}'{{field}}',{% endfor %} 47 | ] 48 | {% elif model.api_config.get('filter') and not model.api_config.get('search') %} 49 | filter_backends = [DjangoFilterBackend,] 50 | filterset_fields = [ 51 | {% for field in model.filter_fields %}'{{field}}',{% endfor %} 52 | ] 53 | {% elif not model.api_config.get('filter') and model.api_config.get('search') %} 54 | filter_backends = [SearchFilter,] 55 | search_fields = [ 56 | {% for field in model.search_fields %}'{{field}}',{% endfor %} 57 | ] 58 | {% endif %} 59 | {% endif %} 60 | {% if cache_support %} 61 | model_class = {{model.name}} 62 | 63 | def get_queryset(self): 64 | """ 65 | get queryset from cache 66 | """ 67 | return self.model_class.get_all_from_cache() 68 | 69 | def get_object(self): 70 | """ 71 | get object from cache 72 | """ 73 | queryset = self.get_queryset() 74 | lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field 75 | filter_kwargs = { 76 | self.lookup_field: int(self.kwargs[lookup_url_kwarg]) if self.kwargs[lookup_url_kwarg].isdigit() else None 77 | } 78 | qs = self.model_class.filter_from_cache(queryset, **filter_kwargs) 79 | if len(qs) == 0: 80 | raise Http404('Not Found') 81 | obj = qs[0] 82 | return obj 83 | {% else %} 84 | queryset = {{model.name}}.objects.all() 85 | {% endif %} 86 | {% endfor %} -------------------------------------------------------------------------------- /sage_painless/services/readme_generator.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - README Generator 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | import os 7 | import time 8 | from platform import python_version 9 | 10 | from django.conf import settings 11 | 12 | # Base 13 | from sage_painless.services.abstract import AbstractReadMeGenerator 14 | 15 | # Helpers 16 | from sage_painless.utils.file_service import FileService 17 | from sage_painless.utils.git_service import GitSupport 18 | from sage_painless.utils.jinja_service import JinjaHandler 19 | from sage_painless.utils.json_service import JsonHandler 20 | from sage_painless.utils.pep8_service import Pep8 21 | from sage_painless.utils.timing_service import TimingService 22 | 23 | from sage_painless import templates 24 | 25 | 26 | class ReadMeGenerator(AbstractReadMeGenerator, JinjaHandler, JsonHandler, Pep8, FileService, TimingService, GitSupport): 27 | """Generate README.md for project""" 28 | README_TEMPLATE = 'README.jinja' 29 | 30 | def __init__(self, *args, **kwargs): 31 | """init""" 32 | super().__init__(*args, **kwargs) 33 | 34 | def generate(self, diagram_path, git_support=False): 35 | """stream README.md to docs/sage_painless/git/README.md 36 | template: 37 | sage_painless/templates/README.jinja 38 | """ 39 | start_time = time.time() 40 | diagram = self.load_json(diagram_path) 41 | 42 | # initialize 43 | self.create_dir_if_not_exists('docs') 44 | self.create_dir_if_not_exists('docs/sage_painless') 45 | self.create_dir_if_not_exists('docs/sage_painless/git') 46 | if git_support: 47 | self.init_repo(settings.BASE_DIR) 48 | 49 | modules = self.get_installed_module_names() 50 | django_apps = self.get_built_in_app_names() 51 | 52 | project_name = self.get_project_name() 53 | project_version = self.get_project_version() 54 | 55 | django_version = self.get_django_version() 56 | py_version = python_version() 57 | 58 | project_structure = self.make_tree(settings.BASE_DIR) 59 | 60 | self.stream_to_template( 61 | output_path=f'{settings.BASE_DIR}/docs/sage_painless/git/README.md', 62 | template_path=os.path.abspath(templates.__file__).replace('__init__.py', self.README_TEMPLATE), 63 | data={ 64 | 'project_name': project_name, 65 | 'built_in_apps': django_apps, 66 | 'installed_modules': modules, 67 | 'django_version': django_version, 68 | 'py_version': py_version, 69 | 'project_version': project_version, 70 | 'docker_support': self.has_docker_support(), 71 | 'structure': project_structure 72 | } 73 | ) 74 | if git_support: 75 | self.commit_file( 76 | f'{settings.BASE_DIR}/docs/sage_painless/git/README.md', 77 | f'docs (git): Create README.md' 78 | ) 79 | 80 | end_time = time.time() 81 | return True, 'README generated ({:.3f} ms)'.format(self.calculate_execute_time(start_time, end_time)) 82 | -------------------------------------------------------------------------------- /sage_painless/services/test_generator.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - Test Generator 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | import os 7 | import time 8 | 9 | from django.conf import settings 10 | 11 | # Base 12 | from sage_painless.services.abstract import AbstractTestGenerator 13 | 14 | # Helpers 15 | from sage_painless.utils.file_service import FileService 16 | from sage_painless.utils.git_service import GitSupport 17 | from sage_painless.utils.jinja_service import JinjaHandler 18 | from sage_painless.utils.json_service import JsonHandler 19 | from sage_painless.utils.pep8_service import Pep8 20 | from sage_painless.utils.timing_service import TimingService 21 | 22 | from sage_painless import templates 23 | 24 | 25 | class TestGenerator(AbstractTestGenerator, JinjaHandler, JsonHandler, Pep8, FileService, TimingService, GitSupport): 26 | """Generate model/api tests for given diagram""" 27 | TEST_TEMPLATE = 'test.jinja' 28 | 29 | def __init__(self, *args, **kwargs): 30 | """init""" 31 | super().__init__(*args, **kwargs) 32 | 33 | def generate( 34 | self, diagram_path: str, app_name: str, model_test: bool = True, 35 | api_test: bool = True, git_support: bool = False): 36 | """stream tests to app_name/tests/test_model_name.py 37 | template: 38 | sage_painless/templates/test.jinja 39 | """ 40 | start_time = time.time() 41 | diagram = self.load_json(diagram_path) 42 | if git_support: 43 | self.init_repo(settings.BASE_DIR) 44 | 45 | models_diagram = diagram.get( 46 | self.get_constant('APPS_KEYWORD')).get(app_name).get( 47 | self.get_constant('MODELS_KEYWORD')) # get models data for current app 48 | models, signals = self.extract_models(models_diagram) 49 | 50 | self.create_app_if_not_exists(app_name) 51 | self.create_dir_for_app_if_not_exists(self.get_constant('TESTS_DIR'), app_name) 52 | self.create_file_if_not_exists(f'{settings.BASE_DIR}/{app_name}/tests/__init__.py') 53 | self.delete_file_if_exists(f'{settings.BASE_DIR}/{app_name}/tests.py') 54 | 55 | for model in models: 56 | # generate model tests 57 | self.stream_to_template( 58 | output_path=f'{settings.BASE_DIR}/{app_name}/tests/test_{model.name.lower()}.py', 59 | template_path=os.path.abspath(templates.__file__).replace('__init__.py', self.TEST_TEMPLATE), 60 | data={ 61 | 'app_name': app_name, 62 | 'models': models, 63 | 'model': model, 64 | 'api_test': api_test, 65 | 'model_test': model_test, 66 | 'signals': self.filter_signals_for_model(signals, model), 67 | 'stream': self.check_streaming_support([model]) 68 | } 69 | ) 70 | self.fix_pep8(f'{settings.BASE_DIR}/{app_name}/tests/test_{model.name.lower()}.py') 71 | if git_support: 72 | self.commit_file( 73 | f'{settings.BASE_DIR}/{app_name}/tests/test_{model.name.lower()}.py', 74 | f'test ({app_name}--{model.name.lower()}): Test model & API' 75 | ) 76 | end_time = time.time() 77 | return True, 'tests generated ({:.3f} ms)'.format(self.calculate_execute_time(start_time, end_time)) 78 | -------------------------------------------------------------------------------- /tests/success/test_admin_generator.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.test import TestCase 4 | from django.conf import settings 5 | 6 | from sage_painless.classes.admin import Admin 7 | from sage_painless.services.admin_generator import AdminGenerator 8 | from sage_painless.utils.json_service import JsonHandler 9 | 10 | from tests import fixtures 11 | 12 | 13 | class TestAdminGenerator(TestCase): 14 | def setUp(self) -> None: 15 | self.json_handler = JsonHandler() 16 | self.app_name = 'products' 17 | self.admin_generator = AdminGenerator() 18 | self.diagram_path = os.path.abspath(fixtures.__file__).replace('__init__.py', 'product_fixture.json') 19 | self.diagram = self.json_handler.load_json(self.diagram_path).get('apps').get(self.app_name).get('models') 20 | 21 | def get_diagram_admins(self, diagram): 22 | admins = list() 23 | for table_name in diagram.keys(): 24 | table = diagram.get(table_name) 25 | admin_data = self.admin_generator.get_table_admin(table) 26 | 27 | admin = Admin() 28 | admin.model = table_name 29 | for key in admin_data.keys(): 30 | value = admin_data.get(key) 31 | setattr(admin, key, value) 32 | 33 | admins.append(admin) 34 | 35 | return admins 36 | 37 | def check_field_value(self, list1, list2, field): 38 | for item in list1: 39 | for item2 in list2: 40 | if getattr(item, field) == getattr(item2, field): 41 | return True 42 | 43 | return False 44 | 45 | def open_generated_file(self, file_path): 46 | with open(file_path, 'r') as f: 47 | data = f.read() 48 | return data 49 | 50 | def get_obj_properties(self, obj): 51 | return vars(obj) 52 | 53 | def test_extract_admin(self): 54 | admins = self.admin_generator.extract_admin(self.diagram) 55 | test_admins = self.get_diagram_admins(self.diagram) 56 | 57 | self.assertEqual(len(admins), len(test_admins)) 58 | self.assertTrue(self.check_field_value(admins, test_admins, 'model')) 59 | self.assertTrue(self.check_field_value(admins, test_admins, 'list_display')) 60 | self.assertTrue(self.check_field_value(admins, test_admins, 'list_filter')) 61 | self.assertTrue(self.check_field_value(admins, test_admins, 'search_fields')) 62 | self.assertTrue(self.check_field_value(admins, test_admins, 'raw_id_fields')) 63 | self.assertTrue(self.check_field_value(admins, test_admins, 'filter_horizontal')) 64 | self.assertTrue(self.check_field_value(admins, test_admins, 'filter_vertical')) 65 | self.assertTrue(self.check_field_value(admins, test_admins, 'has_add_permission')) 66 | self.assertTrue(self.check_field_value(admins, test_admins, 'has_change_permission')) 67 | self.assertTrue(self.check_field_value(admins, test_admins, 'has_delete_permission')) 68 | 69 | def test_stream_to_jinja(self): 70 | admins = self.admin_generator.extract_admin(self.diagram) 71 | self.admin_generator.generate(self.diagram_path, self.app_name) 72 | admin_data = self.open_generated_file(f'{settings.BASE_DIR}/{self.app_name}/admin.py') 73 | for admin in admins: 74 | for prop in self.get_obj_properties(admin): 75 | if getattr(admin, prop): 76 | self.assertTrue(getattr(admin, prop), admin_data) 77 | -------------------------------------------------------------------------------- /sage_painless/services/tox_generator.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - Tox Config Generator 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | import os 7 | import time 8 | 9 | from django.conf import settings 10 | 11 | # Base 12 | from sage_painless.services.abstract import AbstractToxGenerator 13 | 14 | # Helpers 15 | from sage_painless.utils.git_service import GitSupport 16 | from sage_painless.utils.jinja_service import JinjaHandler 17 | from sage_painless.utils.json_service import JsonHandler 18 | from sage_painless.utils.pep8_service import Pep8 19 | from sage_painless.utils.timing_service import TimingService 20 | 21 | from sage_painless import templates 22 | 23 | 24 | class ToxGenerator(AbstractToxGenerator, JinjaHandler, JsonHandler, Pep8, TimingService, GitSupport): 25 | """generate tox configs & coverage support""" 26 | COVERAGERC_TEMPLATE = 'coveragerc.jinja' 27 | TOX_TEMPLATE = 'tox.jinja' 28 | SETUP_TEMPLATE = 'setup.jinja' 29 | 30 | def __init__(self, *args, **kwargs): 31 | super().__init__(*args, **kwargs) 32 | 33 | def generate(self, diagram_path, git_support=False): 34 | """generate tox and coverage config 35 | template: 36 | sage_painless/templates/tox.jinja 37 | sage_painless/templates/coveragerc.jinja 38 | sage_painless/templates/setup.jinja 39 | """ 40 | start_time = time.time() 41 | diagram = self.load_json(diagram_path) 42 | app_names = self.get_app_names() 43 | kernel_name = self.get_kernel_name() 44 | 45 | config = self.extract_tox_config(diagram) 46 | 47 | if git_support: 48 | self.init_repo(settings.BASE_DIR) 49 | 50 | # .coveragerc 51 | self.stream_to_template( 52 | output_path=f'{settings.BASE_DIR}/.coveragerc', 53 | template_path=os.path.abspath(templates.__file__).replace('__init__.py', self.COVERAGERC_TEMPLATE), 54 | data={ 55 | 'app_names': app_names 56 | } 57 | ) 58 | 59 | # tox.ini 60 | self.stream_to_template( 61 | output_path=f'{settings.BASE_DIR}/tox.ini', 62 | template_path=os.path.abspath(templates.__file__).replace('__init__.py', self.TOX_TEMPLATE), 63 | data={ 64 | 'kernel_name': kernel_name 65 | } 66 | ) 67 | 68 | # setup.py 69 | self.stream_to_template( 70 | output_path=f'{settings.BASE_DIR}/setup.py', 71 | template_path=os.path.abspath(templates.__file__).replace('__init__.py', self.SETUP_TEMPLATE), 72 | data={ 73 | 'kernel_name': kernel_name, 74 | 'config': config, 75 | 'reqs': self.parse_requirements(config.get('req_path')) 76 | } 77 | ) 78 | self.fix_pep8(f'{settings.BASE_DIR}/setup.py') 79 | 80 | if git_support: 81 | self.commit_file( 82 | f'{settings.BASE_DIR}/.coveragerc', 83 | f'docs (coverage): Add coverage config file' 84 | ) 85 | self.commit_file( 86 | f'{settings.BASE_DIR}/tox.ini', 87 | f'docs (tox): Add tox config file' 88 | ) 89 | self.commit_file( 90 | f'{settings.BASE_DIR}/setup.py', 91 | f'docs (python): Create setup file' 92 | ) 93 | 94 | end_time = time.time() 95 | return True, 'Tox config generated ({:.3f} ms)'.format(self.calculate_execute_time(start_time, end_time)) 96 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code Of Conduct 2 | 3 | **Like the technical community as a whole, the Sage team and community is made up of a mixture of professionals, working on every aspect of the mission - including mentorship, teaching, and connecting people.** 4 | 5 | * Diversity is one of our huge strengths, but it can also lead to communication issues and unhappiness. To that end, we have a few ground rules that we ask people to adhere to. This code applies equally to founders, mentors and those seeking help and guidance. 6 | 7 | * This isn’t an exhaustive list of things that you can’t do. Rather, take it in the spirit in which it’s intended - a guide to make it easier to enrich all of us and the technical communities in which we participate. 8 | 9 | * if you believe someone is violating the code of conduct, we ask that you report it by emailing mail@sageteam.com 10 | 11 | 1. **Be friendly and patient**. 12 | 2. **Be welcoming**. We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability. 13 | 3. **Be considerate**. Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that we're a world-wide community, so you might not be communicating in someone else's primary language. 14 | 4. **Be respectful**. Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It’s important to remember that a community where people feel uncomfortable or threatened is not a productive one. Members of the Django community should be respectful when dealing with other members as well as with people outside the Django community. 15 | 5. **Be careful in the words that you choose**. We are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren't acceptable. This includes, but is not limited to: 16 | 17 | - Violent threats or language directed against another person. 18 | - Discriminatory jokes and language. 19 | - sexually explicit or violent material. 20 | - Posting (or threatening to post) other people's personally identifying information ("doxing"). 21 | - Personal insults, especially those using racist or sexist terms. 22 | - Unwelcome sexual attention. 23 | - Advocating for, or encouraging, any of the above behavior. 24 | - Repeated harassment of others. In general, if someone asks you to stop, then stop. 25 | 26 | 6. **When we disagree, try to understand why**. Disagreements, both social and technical, happen all the time and Django is no exception. It is important that we resolve disagreements and differing views constructively. Remember that we’re different. The strength of Django comes from its varied community, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn’t mean that they’re wrong. Don’t forget that it is human to err and blaming each other doesn’t get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes. -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [1.13.0] - 2021-08-17 6 | ### Added 7 | - Added package manager support 8 | 9 | ## [1.12.0] - 2021-08-13 10 | ### Added 11 | - Added git commit generator 12 | 13 | ## [1.11.0] - 2021-08-11 14 | ### Added 15 | - Added nginx config generator 16 | - Added nginx container in docker 17 | 18 | ## [1.10.2] - 2021-08-06 19 | ### Fixed 20 | - Fixed gunicorn default config 21 | - Integrate gunicorn & uwsgi to docker generator 22 | - Create .env file generation in docker 23 | - Fixed some security bugs 24 | 25 | ## [1.10.1] - 2021-08-06 26 | ### Fixed 27 | - Fixed deploy generator 28 | - Refactored deploy generator 29 | 30 | ## [1.10.0] - 2021-08-06 31 | ### Added 32 | - Added uwsgi config generator 33 | 34 | ## [1.9.0] - 2021-08-05 35 | ### Added 36 | - Added gunicorn config generator 37 | 38 | ## [1.8.0] - 2021-08-04 39 | ### Added 40 | - Added tox & coverage config generator 41 | 42 | ## [1.7.0] - 2021-08-03 43 | ### Added 44 | - Improved test generation 45 | 46 | ## [1.6.0] - 2021-07-24 47 | ### Added 48 | - Added video stream support 49 | 50 | ## [1.5.0] - 2021-07-23 51 | ### Added 52 | - Added diagram format validator 53 | 54 | ## [1.4.0] - 2021-07-21 55 | ### Added 56 | - Added README.md generator 57 | ### Fixed 58 | - Fixed reporter 59 | 60 | ## [1.3.0] - 2021-07-18 61 | ### Added 62 | - Added multi app generation support 63 | - Added m2m field 64 | 65 | ## [1.2.1] - 2021-07-17 66 | ### Fixed 67 | - Fixed management command 68 | 69 | ## [1.2.0] - 2021-07-17 70 | ### Added 71 | - Added API method support 72 | 73 | ## [1.1.1] - 2021-07-17 74 | ### Fixed 75 | - Change report file name pattern 76 | 77 | ## [1.1.0] - 2021-07-15 78 | ### Added 79 | - Generate models in multiple files 80 | ### Fixed 81 | - Fixed verbose name generation 82 | - Changed docs directory 83 | 84 | ## [1.0.0] - 2021-07-15 85 | ### Added 86 | - Added specific import support (not using *) 87 | - Added multiple file generation (mixins.py, service.py, signals.py, ...) 88 | - Added validate required setting in management command 89 | - Added log user's answers for each app generation in docs 90 | 91 | [1.0.0]: https://github.com/sageteam-org/django-sage-painless/commits/develop 92 | [1.1.0]: https://github.com/sageteam-org/django-sage-painless/commits/develop 93 | [1.1.1]: https://github.com/sageteam-org/django-sage-painless/commits/develop 94 | [1.2.0]: https://github.com/sageteam-org/django-sage-painless/commits/develop 95 | [1.2.1]: https://github.com/sageteam-org/django-sage-painless/commits/develop 96 | [1.3.0]: https://github.com/sageteam-org/django-sage-painless/commits/develop 97 | [1.4.0]: https://github.com/sageteam-org/django-sage-painless/commits/develop 98 | [1.5.0]: https://github.com/sageteam-org/django-sage-painless/commits/develop 99 | [1.6.0]: https://github.com/sageteam-org/django-sage-painless/commits/develop 100 | [1.7.0]: https://github.com/sageteam-org/django-sage-painless/commits/develop 101 | [1.8.0]: https://github.com/sageteam-org/django-sage-painless/commits/develop 102 | [1.9.0]: https://github.com/sageteam-org/django-sage-painless/commits/develop 103 | [1.10.0]: https://github.com/sageteam-org/django-sage-painless/commits/develop 104 | [1.10.1]: https://github.com/sageteam-org/django-sage-painless/commits/develop 105 | [1.10.2]: https://github.com/sageteam-org/django-sage-painless/commits/develop 106 | [1.11.0]: https://github.com/sageteam-org/django-sage-painless/commits/develop 107 | [1.12.0]: https://github.com/sageteam-org/django-sage-painless/commits/develop 108 | [1.13.0]: https://github.com/sageteam-org/django-sage-painless/commits/develop -------------------------------------------------------------------------------- /sage_painless/docs/diagrams/product_diagram.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": { 3 | "ecommerce": { 4 | "models": { 5 | "Category": { 6 | "fields": { 7 | "title": { 8 | "type": "character", 9 | "max_length": 255, 10 | "unique": true 11 | }, 12 | "created": { 13 | "type": "datetime", 14 | "auto_now_add": true 15 | }, 16 | "modified": { 17 | "type": "datetime", 18 | "auto_now": true 19 | } 20 | }, 21 | "admin": { 22 | "list_display": [ 23 | "title", 24 | "created", 25 | "modified" 26 | ], 27 | "list_filter": [ 28 | "created", 29 | "modified" 30 | ], 31 | "search_fields": [ 32 | "title" 33 | ] 34 | }, 35 | "api": { 36 | "methods": [ 37 | "GET", 38 | "POST", 39 | "PUT", 40 | "PATCH", 41 | "DELETE" 42 | ] 43 | } 44 | }, 45 | "Product": { 46 | "fields": { 47 | "title": { 48 | "type": "character", 49 | "max_length": 255 50 | }, 51 | "description": { 52 | "type": "character", 53 | "max_length": 255 54 | }, 55 | "price": { 56 | "type": "integer" 57 | }, 58 | "category": { 59 | "type": "fk", 60 | "to": "Category", 61 | "related_name": "'products'", 62 | "on_delete": "CASCADE" 63 | }, 64 | "created": { 65 | "type": "datetime", 66 | "auto_now_add": true 67 | }, 68 | "modified": { 69 | "type": "datetime", 70 | "auto_now": true 71 | } 72 | }, 73 | "admin": { 74 | "list_display": [ 75 | "title", 76 | "price", 77 | "category" 78 | ], 79 | "list_filter": [ 80 | "created", 81 | "modified" 82 | ], 83 | "search_fields": [ 84 | "title", 85 | "description" 86 | ], 87 | "raw_id_fields": [ 88 | "category" 89 | ] 90 | } 91 | } 92 | } 93 | }, 94 | "discount": { 95 | "models": { 96 | "Discount": { 97 | "fields": { 98 | "product": { 99 | "type": "fk", 100 | "to": "Product", 101 | "related_name": "'discounts'", 102 | "on_delete": "CASCADE" 103 | }, 104 | "discount": { 105 | "type": "integer" 106 | }, 107 | "created": { 108 | "type": "datetime", 109 | "auto_now_add": true 110 | }, 111 | "modified": { 112 | "type": "datetime", 113 | "auto_now": true 114 | } 115 | }, 116 | "admin": { 117 | "list_display": [ 118 | "discount", 119 | "product", 120 | "created", 121 | "modified" 122 | ], 123 | "list_filter": [ 124 | "created", 125 | "modified" 126 | ], 127 | "raw_id_fields": [ 128 | "product" 129 | ] 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /tests/fixtures/product_fixture.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": { 3 | "products": { 4 | "models": { 5 | "Category": { 6 | "fields": { 7 | "title": { 8 | "type": "character", 9 | "max_length": 255, 10 | "unique": true, 11 | "encrypt": false 12 | }, 13 | "created": { 14 | "type": "datetime", 15 | "auto_now_add": true 16 | }, 17 | "modified": { 18 | "type": "datetime", 19 | "auto_now": true 20 | } 21 | }, 22 | "admin": { 23 | "list_display": [ 24 | "title", 25 | "created", 26 | "modified" 27 | ], 28 | "list_filter": [ 29 | "created", 30 | "modified" 31 | ], 32 | "search_fields": [ 33 | "title" 34 | ] 35 | }, 36 | "api": { 37 | "methods": [ 38 | "GET", 39 | "POST", 40 | "PUT", 41 | "PATCH", 42 | "DELETE" 43 | ], 44 | "permission": "any", 45 | "search": true, 46 | "filter": true 47 | } 48 | }, 49 | "Product": { 50 | "fields": { 51 | "title": { 52 | "type": "character", 53 | "max_length": 255 54 | }, 55 | "description": { 56 | "type": "character", 57 | "max_length": 255 58 | }, 59 | "price": { 60 | "type": "integer" 61 | }, 62 | "category": { 63 | "type": "fk", 64 | "to": "Category", 65 | "related_name": "'products'", 66 | "on_delete": "CASCADE" 67 | }, 68 | "created": { 69 | "type": "datetime", 70 | "auto_now_add": true 71 | }, 72 | "modified": { 73 | "type": "datetime", 74 | "auto_now": true 75 | } 76 | }, 77 | "admin": { 78 | "list_display": [ 79 | "title", 80 | "price", 81 | "category" 82 | ], 83 | "list_filter": [ 84 | "created", 85 | "modified" 86 | ], 87 | "search_fields": [ 88 | "title", 89 | "description" 90 | ], 91 | "raw_id_fields": [ 92 | "category" 93 | ] 94 | } 95 | }, 96 | "Discount": { 97 | "fields": { 98 | "product": { 99 | "type": "fk", 100 | "to": "Product", 101 | "related_name": "'discounts'", 102 | "on_delete": "CASCADE" 103 | }, 104 | "discount": { 105 | "type": "integer" 106 | }, 107 | "created": { 108 | "type": "datetime", 109 | "auto_now_add": true 110 | }, 111 | "modified": { 112 | "type": "datetime", 113 | "auto_now": true 114 | } 115 | }, 116 | "admin": { 117 | "list_display": [ 118 | "discount", 119 | "product", 120 | "created", 121 | "modified" 122 | ], 123 | "list_filter": [ 124 | "created", 125 | "modified" 126 | ], 127 | "raw_id_fields": [ 128 | "product" 129 | ] 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /tests/success/test_test_generator.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.test import TestCase 4 | 5 | from sage_painless.classes.field import Field 6 | from sage_painless.services.test_generator import TestGenerator 7 | from sage_painless.utils.json_service import JsonHandler 8 | 9 | from tests import fixtures 10 | 11 | 12 | class TestTestGenerator(TestCase): 13 | def setUp(self) -> None: 14 | self.json_handler = JsonHandler() 15 | self.app_name = 'products' 16 | self.test_generator = TestGenerator() 17 | self.diagram_path = os.path.abspath(fixtures.__file__).replace('__init__.py', 'product_fixture.json') 18 | self.diagram = self.json_handler.load_json(self.diagram_path).get('apps').get(self.app_name).get('models') 19 | self.field = Field() 20 | self.field_types = self.field.field_types 21 | 22 | def get_table_names(self): 23 | return self.diagram.keys() 24 | 25 | def get_table_field_names(self, table_name): 26 | return self.diagram.get(table_name).get('fields').keys() 27 | 28 | def get_field_type(self, table_name, field_name): 29 | return self.diagram.get(table_name).get('fields').get(field_name).get('type') 30 | 31 | def get_field_attr_names(self, table_name, field_name): 32 | attrs = list(self.diagram.get(table_name).get('fields').get(field_name).keys()) 33 | attrs.remove('type') 34 | return attrs 35 | 36 | def get_attr_value(self, table_name, field_name, attr_name): 37 | return self.diagram.get(table_name).get('fields').get(field_name).get(attr_name) 38 | 39 | def get_table_signals(self): 40 | signals = list() 41 | for table_name in self.diagram.keys(): 42 | table = self.diagram.get(table_name) 43 | fields = self.test_generator.get_table_fields(table) 44 | for field_name in fields.keys(): 45 | field_data = fields.get(field_name) 46 | for key in field_data.keys(): 47 | if key == self.test_generator.TYPE_KEYWORD: 48 | if field_data.get(self.test_generator.TYPE_KEYWORD) == 'one2one': 49 | signals.append(field_name) 50 | return signals 51 | 52 | def test_extract_models(self): 53 | models, signals = self.test_generator.extract_models(self.diagram) 54 | 55 | self.assertEqual(len(models), len(self.get_table_names())) 56 | 57 | for model in models: 58 | self.assertIn(model.name, self.get_table_names()) 59 | for field in model.fields: 60 | self.assertIn( 61 | field.name, self.get_table_field_names(model.name) 62 | ) 63 | self.assertEqual( 64 | field.type, self.field_types.get( 65 | self.get_field_type( 66 | model.name, 67 | field.name 68 | ) 69 | ).get('type') 70 | ) 71 | self.assertEqual( 72 | len(field.attrs), len(self.get_field_attr_names( 73 | model.name, 74 | field.name 75 | )) 76 | ) 77 | for attribute in field.attrs: 78 | self.assertIn( 79 | attribute.key, self.get_field_attr_names( 80 | model.name, 81 | field.name 82 | ) 83 | ) 84 | self.assertEqual( 85 | attribute.value, self.get_attr_value( 86 | model.name, 87 | field.name, 88 | attribute.key 89 | ) 90 | ) 91 | 92 | self.assertEqual(len(signals), len(self.get_table_signals())) 93 | 94 | for signal in signals: 95 | self.assertIn(signal.field, self.get_table_signals()) 96 | self.assertIn(signal.model_a, self.get_table_names()) 97 | self.assertEqual(signal.model_b, self.get_attr_value(signal.model_a, signal.field, 'to')) 98 | -------------------------------------------------------------------------------- /docs/faq.rst: -------------------------------------------------------------------------------- 1 | FAQ 2 | =========== 3 | 4 | **What is code generator?** 5 | -------------------------------- 6 | 7 | A code generator is a tool or resource that generates a particular sort of code or computer programming language. This has many specific meanings in the world of IT, many of them related to the sometimes complex processes of converting human programming syntax to the machine language that can be read by a computing system.One of the most common and conventional uses of the term “code generator” involves other resources or tools that help to turn out specific kinds of code. For example, some homemade or open source code generators can generate classes and methods for easier or more convenient computer programming. This type of resource might also be called a component generator. 8 | 9 | **What is django-sage-painless?** 10 | ------------------------------------------------ 11 | 12 | The django-sage-painless is a valuable package based on Django Web Framework & Django Rest Framework for high-level and rapid web development. The introduced package generates Django applications. After completing many projects, we concluded that any basic project and essential part is its database structure. You can give the database schema in this package and get some parts of the Django application, such as API, models, admin, signals, model cache, setting configuration, mixins, etc. All of these capabilities come with a unit test. So you no longer have to worry about the simple parts of Django, and now you can write your advanced services in Django. Django-sage-painless dramatically speeds up the initial development of the project in Django. However, we intend to make it possible to use it in projects that are in progress. But the reality now is that we have made Django a quick start. We used the name painless instead of the Django code generator because this package allows you to reach your goals with less effort. 13 | 14 | **Why should we use this package?** 15 | ------------------------------------------------ 16 | 17 | One of the most important reasons to use this package is to speed up the development of Django applications. Then, another important reason is that you can use many features with this package if you want. Therefore, you DO NOT have to use all the features of the generator. 18 | 19 | **What are the main features of the package?** 20 | ------------------------------------------------ 21 | 22 | - Generate models based on your defined diagram 23 | - Support database relationships: [one-to-one] [one-to-many] [many-to-many] 24 | - Generate cache mixin to your models (OPTIONAL) 25 | - Generate model test 26 | - Generate signals (if you use one-to-one relationship) 27 | - Generate rest framework API endpoints (OPTIONAL) 28 | - Generate rest framework documentation (OPTIONAL) 29 | - Generate API URLs (if request for API) 30 | - Generate API test 31 | - Generate admin via filter and search capability (OPTIONAL) 32 | - Generate setting configuration of (Redis, RabbitMQ, Celery, etc. OPTIONAL) 33 | - Generate docker compose file, Dockerfile and related documentation (OPTIONAL) 34 | 35 | **Why don't we produce the whole Django project?** 36 | ---------------------------------------------------------------- 37 | 38 | Based on this question, we took a new attitude was taken in the package. One of the important issues in package design is that it is scalable and compatible with projects that are under development. That's why we decided to automate only the apps according to the project design model instead of producing a complete Django project. Therefore, anyone can use this package in the middle of their startup development and release their new features faster than before. 39 | 40 | **How to learn to create a diagram?** 41 | ------------------------------------------------ 42 | 43 | In the example section, we have taught all the sections related to Digram. 44 | 45 | **How does the cache algorithm work?** 46 | ------------------------------------------------ 47 | 48 | Caching algorithm works in such a way that once your data is loaded, it is cached in Redis, and there is no need to query the database again. We have also designed the algorithm like that if your data in the database changes, cached data will be deleted automatically from Redis. 49 | -------------------------------------------------------------------------------- /products/api/views.py: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated views.py 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | 6 | from django.http import Http404 7 | 8 | from rest_framework.viewsets import ModelViewSet 9 | 10 | # permission support 11 | from rest_framework import permissions 12 | 13 | 14 | # filter support 15 | from django_filters.rest_framework import DjangoFilterBackend 16 | 17 | 18 | # search support 19 | from rest_framework.filters import SearchFilter 20 | 21 | 22 | from products.models.category import Category 23 | 24 | from products.models.product import Product 25 | 26 | from products.models.discount import Discount 27 | 28 | from products.api.serializers import ( 29 | CategorySerializer, 30 | ProductSerializer, 31 | DiscountSerializer, 32 | 33 | ) 34 | 35 | 36 | class CategoryViewset(ModelViewSet): 37 | """ 38 | Category Viewset 39 | Auto generated 40 | """ 41 | serializer_class = CategorySerializer 42 | 43 | http_method_names = ['get', 'post', 'put', 'patch', 'delete'] 44 | permission_classes = (permissions.AllowAny,) 45 | 46 | filter_backends = [DjangoFilterBackend, SearchFilter] 47 | search_fields = [ 48 | 'title', 'created', 'modified', 49 | ] 50 | filterset_fields = [ 51 | 'title', 'created', 'modified', 52 | ] 53 | 54 | model_class = Category 55 | 56 | def get_queryset(self): 57 | """ 58 | get queryset from cache 59 | """ 60 | return self.model_class.get_all_from_cache() 61 | 62 | def get_object(self): 63 | """ 64 | get object from cache 65 | """ 66 | queryset = self.get_queryset() 67 | lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field 68 | filter_kwargs = { 69 | self.lookup_field: int( 70 | self.kwargs[lookup_url_kwarg]) if self.kwargs[lookup_url_kwarg].isdigit() else None 71 | } 72 | qs = self.model_class.filter_from_cache(queryset, **filter_kwargs) 73 | if len(qs) == 0: 74 | raise Http404('Not Found') 75 | obj = qs[0] 76 | return obj 77 | 78 | 79 | class ProductViewset(ModelViewSet): 80 | """ 81 | Product Viewset 82 | Auto generated 83 | """ 84 | serializer_class = ProductSerializer 85 | 86 | model_class = Product 87 | 88 | def get_queryset(self): 89 | """ 90 | get queryset from cache 91 | """ 92 | return self.model_class.get_all_from_cache() 93 | 94 | def get_object(self): 95 | """ 96 | get object from cache 97 | """ 98 | queryset = self.get_queryset() 99 | lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field 100 | filter_kwargs = { 101 | self.lookup_field: int( 102 | self.kwargs[lookup_url_kwarg]) if self.kwargs[lookup_url_kwarg].isdigit() else None 103 | } 104 | qs = self.model_class.filter_from_cache(queryset, **filter_kwargs) 105 | if len(qs) == 0: 106 | raise Http404('Not Found') 107 | obj = qs[0] 108 | return obj 109 | 110 | 111 | class DiscountViewset(ModelViewSet): 112 | """ 113 | Discount Viewset 114 | Auto generated 115 | """ 116 | serializer_class = DiscountSerializer 117 | 118 | model_class = Discount 119 | 120 | def get_queryset(self): 121 | """ 122 | get queryset from cache 123 | """ 124 | return self.model_class.get_all_from_cache() 125 | 126 | def get_object(self): 127 | """ 128 | get object from cache 129 | """ 130 | queryset = self.get_queryset() 131 | lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field 132 | filter_kwargs = { 133 | self.lookup_field: int( 134 | self.kwargs[lookup_url_kwarg]) if self.kwargs[lookup_url_kwarg].isdigit() else None 135 | } 136 | qs = self.model_class.filter_from_cache(queryset, **filter_kwargs) 137 | if len(qs) == 0: 138 | raise Http404('Not Found') 139 | obj = qs[0] 140 | return obj 141 | 142 | -------------------------------------------------------------------------------- /sage_painless/templates/mixins.jinja: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated mixins.py 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | {% if cache_support %} 6 | from django.core.cache import cache 7 | 8 | 9 | class ModelCacheMixin: 10 | """ 11 | Mixin for models that adds filtering on cached queryset. 12 | CACHE_KEY,CACHED_RELATED_OBJECT are required class variables at the inheriting class 13 | CACHE_KEY: 14 | String - key name used for caching the queryset 15 | CACHED_RELATED_OBJECT: 16 | List - list of foreign key attributes for model that needs to be cached 17 | """ 18 | 19 | @classmethod 20 | def get_all_from_cache(cls): 21 | """ 22 | Returns all instances stored in cache 23 | :return: List of Model instances. 24 | """ 25 | if not hasattr(cls, 'CACHE_KEY'): 26 | raise AttributeError("CACHE_KEY must be defined in {}".format(cls.__name__)) 27 | 28 | if hasattr(cls, 'CACHED_RELATED_OBJECT'): 29 | queryset = cache.get_or_set( 30 | cls.CACHE_KEY, 31 | cls.objects.all().select_related(*cls.CACHED_RELATED_OBJECT), 32 | ) 33 | else: 34 | queryset = cache.get_or_set(cls.CACHE_KEY, cls.objects.all()) 35 | return queryset 36 | 37 | @classmethod 38 | def filter_from_cache(cls, queryset=None, **kwargs): 39 | """ 40 | Filters and returns Model instances from cache. 41 | It currently supports 2 types of filter 42 | 1. Equality Filter - e.g id = 1 and name = 'test' 43 | filter_from_cache(id=1, name= 'test') 44 | 2. In List Filter - e.g id in [1,2,3] 45 | filter_from_cache(id= [1,2,3]) 46 | :param queryset: list of model instances that needs to be filtered. 47 | If not present, filtering is done on all cached instances 48 | :param kwargs: dictionary containing filter property and values. 49 | :return: List containing Model objects 50 | """ 51 | if not queryset: 52 | queryset = cls.get_all_from_cache() 53 | 54 | def filter_obj(obj): 55 | select = True # boolean indicating if the item should be selected 56 | for filter_key, filter_value in kwargs.items(): 57 | if isinstance(filter_value, list): 58 | if getattr(obj, filter_key) not in filter_value: 59 | select = False 60 | break 61 | elif getattr(obj, filter_key) != filter_value: 62 | select = False 63 | break 64 | return select 65 | 66 | return list(filter(filter_obj, queryset)) 67 | 68 | @classmethod 69 | def filter_related_from_cache(cls, queryset=None, **kwargs): 70 | """ 71 | Filtering is based on model's foreign keys, 72 | which is set as CACHED_RELATED_OBJECT in model class. 73 | It currently supports 2 types of filter 74 | 1. Equality Filter for foreign key's table- e.g id = 1 and name = 'test' 75 | filter_from_cache(foreign_key= {"name": "test", "id": 5}) 76 | 2. In List Filter for foreign key's table- e.g id in [1,2,3] 77 | filter_from_cache(foreign_key={'id': [1,2,3]}) 78 | :param queryset: list of model instances that needs to be filtered. 79 | :param kwargs: dictionary containing filter property and values. 80 | :return: List containing Model objects 81 | """ 82 | if not queryset: 83 | queryset = cls.get_all_from_cache() 84 | 85 | for foreign_key, related_filters in kwargs.items(): 86 | related_objects_list = [getattr(obj, foreign_key) for obj in queryset] 87 | # Filtering related objects based related object's attribute filters 88 | filtered_related_objects = cls.filter_from_cache( 89 | related_objects_list, **related_filters 90 | ) 91 | # Filtering queryset based filtered related objects 92 | queryset = list( 93 | filter( 94 | lambda x: getattr(x, foreign_key) in filtered_related_objects, 95 | queryset, 96 | ) 97 | ) 98 | return queryset 99 | {% endif %} -------------------------------------------------------------------------------- /products/mixins.py: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated mixins.py 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | 6 | from django.core.cache import cache 7 | 8 | 9 | class ModelCacheMixin: 10 | """ 11 | Mixin for models that adds filtering on cached queryset. 12 | CACHE_KEY,CACHED_RELATED_OBJECT are required class variables at the inheriting class 13 | CACHE_KEY: 14 | String - key name used for caching the queryset 15 | CACHED_RELATED_OBJECT: 16 | List - list of foreign key attributes for model that needs to be cached 17 | """ 18 | 19 | @classmethod 20 | def get_all_from_cache(cls): 21 | """ 22 | Returns all instances stored in cache 23 | :return: List of Model instances. 24 | """ 25 | if not hasattr(cls, 'CACHE_KEY'): 26 | raise AttributeError( 27 | "CACHE_KEY must be defined in {}".format( 28 | cls.__name__)) 29 | 30 | if hasattr(cls, 'CACHED_RELATED_OBJECT'): 31 | queryset = cache.get_or_set( 32 | cls.CACHE_KEY, 33 | cls.objects.all().select_related(*cls.CACHED_RELATED_OBJECT), 34 | ) 35 | else: 36 | queryset = cache.get_or_set(cls.CACHE_KEY, cls.objects.all()) 37 | return queryset 38 | 39 | @classmethod 40 | def filter_from_cache(cls, queryset=None, **kwargs): 41 | """ 42 | Filters and returns Model instances from cache. 43 | It currently supports 2 types of filter 44 | 1. Equality Filter - e.g id = 1 and name = 'test' 45 | filter_from_cache(id=1, name= 'test') 46 | 2. In List Filter - e.g id in [1,2,3] 47 | filter_from_cache(id= [1,2,3]) 48 | :param queryset: list of model instances that needs to be filtered. 49 | If not present, filtering is done on all cached instances 50 | :param kwargs: dictionary containing filter property and values. 51 | :return: List containing Model objects 52 | """ 53 | if not queryset: 54 | queryset = cls.get_all_from_cache() 55 | 56 | def filter_obj(obj): 57 | select = True # boolean indicating if the item should be selected 58 | for filter_key, filter_value in kwargs.items(): 59 | if isinstance(filter_value, list): 60 | if getattr(obj, filter_key) not in filter_value: 61 | select = False 62 | break 63 | elif getattr(obj, filter_key) != filter_value: 64 | select = False 65 | break 66 | return select 67 | 68 | return list(filter(filter_obj, queryset)) 69 | 70 | @classmethod 71 | def filter_related_from_cache(cls, queryset=None, **kwargs): 72 | """ 73 | Filtering is based on model's foreign keys, 74 | which is set as CACHED_RELATED_OBJECT in model class. 75 | It currently supports 2 types of filter 76 | 1. Equality Filter for foreign key's table- e.g id = 1 and name = 'test' 77 | filter_from_cache(foreign_key= {"name": "test", "id": 5}) 78 | 2. In List Filter for foreign key's table- e.g id in [1,2,3] 79 | filter_from_cache(foreign_key={'id': [1,2,3]}) 80 | :param queryset: list of model instances that needs to be filtered. 81 | :param kwargs: dictionary containing filter property and values. 82 | :return: List containing Model objects 83 | """ 84 | if not queryset: 85 | queryset = cls.get_all_from_cache() 86 | 87 | for foreign_key, related_filters in kwargs.items(): 88 | related_objects_list = [ 89 | getattr(obj, foreign_key) for obj in queryset] 90 | # Filtering related objects based related object's attribute 91 | # filters 92 | filtered_related_objects = cls.filter_from_cache( 93 | related_objects_list, **related_filters 94 | ) 95 | # Filtering queryset based filtered related objects 96 | queryset = list( 97 | filter( 98 | lambda x: getattr( 99 | x, foreign_key) in filtered_related_objects, 100 | queryset, 101 | ) 102 | ) 103 | return queryset 104 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ----- 3 | 4 | For generating a whole project you just need a diagram. diagram is a 5 | json file that contains information about database tables. 6 | 7 | `you can find examples of diagram file 8 | here `__ 9 | 10 | start to generate (it is required for development. you will run tests on 11 | this app) 12 | 13 | [NEW]: First validate the format of your diagram, It will raise errors if diagram format was incorrect. 14 | 15 | .. code:: shell 16 | 17 | $ python manage.py validate_diagram --diagram 18 | 19 | Now you can generate code 20 | 21 | .. code:: shell 22 | 23 | $ python manage.py generate --diagram 24 | 25 | You can generate deploy config files 26 | 27 | .. code:: shell 28 | 29 | $ python manage.py deploy --diagram 30 | 31 | You can generate docs files 32 | 33 | .. code:: shell 34 | 35 | $ python manage.py docs --diagram 36 | 37 | Here system will ask you what you want to generate for your app. 38 | Questions: 39 | 40 | ====================================================================== ========================================================================== 41 | Question Description 42 | ====================================================================== ========================================================================== 43 | Would you like to generate models.py(yes/no)? generates models.py from json diagram for your app 44 | Would you like to generate admin.py(yes/no)? generates admin.py from admin settings in json diagram for your project 45 | Would you like to generate serializers.py & views.py(yes/no)? generates serializers.py and views.py in api directory for your project 46 | Would you like to generate test for your project(yes/no)? generates model test and api test for your project in tests directory 47 | Would you like to add cache queryset support(yes/no)? it will cache queryset via redis in your views.py 48 | ====================================================================== ========================================================================== 49 | 50 | 51 | If you generated api you have to add app urls to urls.py: 52 | 53 | .. code:: python 54 | 55 | urlpatterns = [ 56 | ... 57 | path('api/', include('products.api.urls')), 58 | ... 59 | ] 60 | 61 | If you set cache support add CACHES to your settings: 62 | 63 | .. code:: python 64 | 65 | REDIS_URL = 'redis://localhost:6379/' 66 | CACHES = { 67 | "default": { 68 | "BACKEND": "django_redis.cache.RedisCache", 69 | "LOCATION": os.environ['REDIS_URL'] if os.environ.get('REDIS_URL') else settings.REDIS_URL if hasattr(settings, 'REDIS_URL') else 'redis://localhost:6379/' 70 | } 71 | } 72 | 73 | If you have encrypted field in diagram: 74 | 75 | - your database should be PostgreSQL 76 | - you should install `pgcrypto` extension for PostgreSQL with this command 77 | 78 | .. code:: shell 79 | 80 | $ sudo -u postgres psql 81 | $ CREATE EXTENSION pgcrypto; 82 | 83 | - You have to migrate your new models 84 | 85 | .. code:: shell 86 | 87 | $ python manage.py makemigrations 88 | $ python manage.py migrate 89 | 90 | - You can run tests for your app 91 | 92 | .. code:: shell 93 | 94 | $ python manage.py test products 95 | 96 | - Django run server 97 | 98 | .. code:: shell 99 | 100 | $ python manage.py runserver 101 | 102 | - For support Rest API doc add this part to your urls.py 103 | 104 | .. code:: python 105 | 106 | from rest_framework.permissions import AllowAny 107 | from drf_yasg.views import get_schema_view 108 | from drf_yasg import openapi 109 | 110 | schema_view = get_schema_view( 111 | openapi.Info( 112 | title="Rest API Doc", 113 | default_version='v1', 114 | description="Auto Generated API Docs", 115 | license=openapi.License(name="S.A.G.E License"), 116 | ), 117 | public=True, 118 | permission_classes=(AllowAny,), 119 | ) 120 | 121 | urlpatterns = [ 122 | ... 123 | path('api/doc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-swagger-ui'), 124 | ... 125 | ] 126 | 127 | - Rest API documentation is available at ``localhost:8000/api/doc/`` 128 | -------------------------------------------------------------------------------- /sage_painless/services/api_generator.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - API Generator 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | import os 7 | import time 8 | 9 | from django.conf import settings 10 | 11 | # Base 12 | from sage_painless.services.abstract import AbstractAPIGenerator 13 | 14 | # Helpers 15 | from sage_painless.utils.file_service import FileService 16 | from sage_painless.utils.git_service import GitSupport 17 | from sage_painless.utils.jinja_service import JinjaHandler 18 | from sage_painless.utils.json_service import JsonHandler 19 | from sage_painless.utils.pep8_service import Pep8 20 | from sage_painless.utils.timing_service import TimingService 21 | 22 | from sage_painless import templates 23 | 24 | 25 | class APIGenerator(AbstractAPIGenerator, JinjaHandler, JsonHandler, Pep8, FileService, TimingService, GitSupport): 26 | """Generate API serializers & viewsets""" 27 | SERIALIZERS_TEMPLATE = 'serializers.jinja' 28 | VIEWS_TEMPLATE = 'views.jinja' 29 | URLS_TEMPLATE = 'urls.jinja' 30 | 31 | def __init__(self, *args, **kwargs): 32 | """init""" 33 | super().__init__(*args, **kwargs) 34 | 35 | def generate(self, diagram_path: str, app_name: str, cache_support: bool = False, git_support: bool = False): 36 | """ 37 | stream serializers to app_name/api/serializers.py 38 | stream viewsets to app_name/api/views.py 39 | stream urls to app_name/api/urls.py 40 | template: 41 | sage_painless/templates/serializers.jinja 42 | sage_painless/templates/views.jinja 43 | sage_painless/templates/urls.jinja 44 | """ 45 | start_time = time.time() 46 | diagram = self.load_json(diagram_path) 47 | 48 | models_diagram = diagram.get( 49 | self.get_constant('APPS_KEYWORD')).get(app_name).get( 50 | self.get_constant('MODELS_KEYWORD')) # get models data for current app 51 | models = self.extract_models(models_diagram) 52 | 53 | # initialization 54 | self.create_app_if_not_exists(app_name) 55 | self.create_dir_for_app_if_not_exists(self.get_constant('API_DIR'), app_name) 56 | if git_support: 57 | self.init_repo(settings.BASE_DIR) 58 | 59 | # stream to serializers.py 60 | self.stream_to_template( 61 | output_path=f'{settings.BASE_DIR}/{app_name}/api/serializers.py', 62 | template_path=os.path.abspath(templates.__file__).replace('__init__.py', self.SERIALIZERS_TEMPLATE), 63 | data={ 64 | 'app_name': app_name, 65 | 'models': models 66 | } 67 | ) 68 | 69 | # stream to views.py 70 | self.stream_to_template( 71 | output_path=f'{settings.BASE_DIR}/{app_name}/api/views.py', 72 | template_path=os.path.abspath(templates.__file__).replace('__init__.py', self.VIEWS_TEMPLATE), 73 | data={ 74 | 'app_name': app_name, 75 | 'models': models, 76 | 'cache_support': cache_support, 77 | 'permission_support': self.check_permission_support(models), 78 | 'filter_support': self.check_filter_support(models), 79 | 'search_support': self.check_search_support(models) 80 | } 81 | ) 82 | 83 | # stream to urls.py 84 | self.stream_to_template( 85 | output_path=f'{settings.BASE_DIR}/{app_name}/api/urls.py', 86 | template_path=os.path.abspath(templates.__file__).replace('__init__.py', self.URLS_TEMPLATE), 87 | data={ 88 | 'app_name': app_name, 89 | 'models': models, 90 | 'streaming_support': self.check_streaming_support(models) 91 | } 92 | ) 93 | 94 | self.fix_pep8(f'{settings.BASE_DIR}/{app_name}/api/serializers.py') 95 | self.fix_pep8(f'{settings.BASE_DIR}/{app_name}/api/views.py') 96 | self.fix_pep8(f'{settings.BASE_DIR}/{app_name}/api/urls.py') 97 | if git_support: 98 | self.commit_file( 99 | f'{settings.BASE_DIR}/{app_name}/api/serializers.py', 100 | f'feat ({app_name}--serializers): Create serializers' 101 | ) 102 | self.commit_file( 103 | f'{settings.BASE_DIR}/{app_name}/api/views.py', 104 | f'feat ({app_name}--views): Create API views' 105 | ) 106 | self.commit_file( 107 | f'{settings.BASE_DIR}/{app_name}/api/urls.py', 108 | f'feat ({app_name}--urls): Add views to urls.py' 109 | ) 110 | 111 | end_time = time.time() 112 | return True, 'API generated ({:.3f} ms)'.format(self.calculate_execute_time(start_time, end_time)) 113 | -------------------------------------------------------------------------------- /sage_painless/templates/test.jinja: -------------------------------------------------------------------------------- 1 | """ 2 | Auto Generated test 3 | Automatically generated with ❤️ by django-sage-painless 4 | """ 5 | from django.apps import apps 6 | from django.urls import reverse 7 | from rest_framework.test import APITestCase 8 | from django_seed import Seed 9 | 10 | {% for model in models %} 11 | from {{app_name}}.models.{{model.name.lower()}} import {{model.name}} 12 | {% endfor %} 13 | 14 | seeder = Seed.seeder() 15 | 16 | class {{model.name.capitalize()}}Test(APITestCase): 17 | """ 18 | {{model.name}} Test 19 | Auto Generated 20 | """ 21 | def setUp(self) -> None: 22 | self.models = apps.get_app_config('{{app_name}}').get_models() 23 | self.models_name = [model._meta.object_name for model in self.models] 24 | {% for model in models %} 25 | {% if not model.has_one_to_one() %} 26 | seeder.add_entity({{model.name}}, 3) 27 | {% endif %} 28 | {% endfor %} 29 | seeder.execute() # create instances 30 | {% if model_test %} 31 | {% if not model.has_one_to_one() %} 32 | def test_{{model.name.lower()}}_model(self): 33 | """test {{model.name}} creation""" 34 | seeder.add_entity({{model.name}}, 1) 35 | seeder.execute() # create instance 36 | # assertions 37 | self.assertTrue({{model.name}}.objects.exists()) 38 | self.assertIn('{{model.name}}', self.models_name) 39 | {% endif %} 40 | {% for signal in signals %} 41 | def test_{{signal.model_a.lower()}}_model_signal(self): 42 | """test {{signal.model_b}} - {{signal.model_a}} signal""" 43 | seeder.add_entity({{signal.model_b}}, 1) 44 | seeder.execute() # create instance 45 | # assertions 46 | self.assertTrue({{signal.model_b}}.objects.exists()) 47 | self.assertTrue({{signal.model_a}}.objects.exists()) 48 | first_object = {{signal.model_b}}.objects.first() 49 | second_object = {{signal.model_a}}.objects.first() 50 | self.assertEqual(second_object.{{signal.field}}, first_object) 51 | {%endfor%} 52 | {% endif %} 53 | {% if api_test %} 54 | {% if model.api_config %} 55 | {% if 'get' in model.api_config.get('methods') %} 56 | def test_{{model.name.lower()}}_list_success(self): 57 | """test {{model.name}} list""" 58 | url = reverse('{{model.name.lower()}}-list') 59 | response = self.client.get(url) 60 | # assertions 61 | self.assertEqual(response.status_code, 200) 62 | if type(response.data) == dict: 63 | if response.data.get('count'): 64 | self.assertGreater(response.data['count'], 0) 65 | else: 66 | self.assertGreater(len(response.data), 0) 67 | 68 | def test_{{model.name.lower()}}_detail_success(self): 69 | """test {{model.name}} detail""" 70 | {{model.name.lower()}} = {{model.name}}.objects.first() 71 | url = reverse('{{model.name.lower()}}-detail', args=[{{model.name.lower()}}.pk]) 72 | response = self.client.get(url) 73 | # assertions 74 | self.assertEqual(response.status_code, 200) 75 | {% for field in model.fields %} 76 | {% if field.type != 'DateTimeField' and field.type != 'DateField' and field.type != 'ForeignKey' and field.type != 'OneToOneField' and field.type != 'ManyToMany' and field.type != 'ImageField' %} 77 | self.assertEqual(response.data.get('{{field.name}}'), {{model.name.lower()}}.{{field.name}}) 78 | {% endif %} 79 | {% endfor %} 80 | {% endif %} 81 | {% else %} 82 | def test_{{model.name.lower()}}_list_success(self): 83 | """test {{model.name}} list""" 84 | url = reverse('{{model.name.lower()}}-list') 85 | response = self.client.get(url) 86 | # assertions 87 | self.assertEqual(response.status_code, 200) 88 | if type(response.data) == dict: 89 | if response.data.get('count'): 90 | self.assertGreater(response.data['count'], 0) 91 | else: 92 | self.assertGreater(len(response.data), 0) 93 | 94 | def test_{{model.name.lower()}}_detail_success(self): 95 | """test {{model.name}} detail""" 96 | {{model.name.lower()}} = {{model.name}}.objects.first() 97 | url = reverse('{{model.name.lower()}}-detail', args=[{{model.name.lower()}}.pk]) 98 | response = self.client.get(url) 99 | # assertions 100 | self.assertEqual(response.status_code, 200) 101 | {% for field in model.fields %} 102 | {% if field.type != 'DateTimeField' and field.type != 'DateField' and field.type != 'ForeignKey' and field.type != 'OneToOneField' and field.type != 'ManyToMany' and field.type != 'ImageField' and field.type != 'FileField' %} 103 | self.assertEqual(response.data.get('{{field.name}}'), {{model.name.lower()}}.{{field.name}}) 104 | {% endif %} 105 | {% endfor %} 106 | {% endif %} 107 | {% if stream %} 108 | def test_{{model.name.lower()}}_streaming_support(self): 109 | """test streaming support""" 110 | url = reverse('video-stream') 111 | 112 | # assertions 113 | self.assertTrue(url) 114 | {% endif %} 115 | {%endif%} -------------------------------------------------------------------------------- /sage_painless/services/docker_generator.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - Docker Generator 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | import os 7 | import time 8 | 9 | from django.conf import settings 10 | from django.core.management.utils import get_random_secret_key 11 | 12 | # Base 13 | from sage_painless.services.abstract import AbstractDockerGenerator 14 | 15 | # Helpers 16 | from sage_painless.utils.git_service import GitSupport 17 | from sage_painless.utils.jinja_service import JinjaHandler 18 | from sage_painless.utils.json_service import JsonHandler 19 | from sage_painless.utils.package_manager_service import PackageManagerSupport 20 | from sage_painless.utils.timing_service import TimingService 21 | 22 | from sage_painless import templates 23 | 24 | 25 | class DockerGenerator(AbstractDockerGenerator, JinjaHandler, JsonHandler, TimingService, GitSupport, PackageManagerSupport): 26 | """Generate DockerFile & docker-compose""" 27 | DOCKERFILE_TEMPLATE = 'Dockerfile.jinja' 28 | DOCKER_COMPOSE_TEMPLATE = 'docker-compose.jinja' 29 | ENV_TEMPLATE = 'env.jinja' 30 | NGINX_TEMPLATE = 'nginx.jinja' 31 | 32 | def __init__(self, *args, **kwargs): 33 | super().__init__(*args, **kwargs) 34 | 35 | def generate( 36 | self, diagram_path, gunicorn_support=False, uwsgi_support=False, 37 | nginx_support=False, git_support=False, package_manager_support=False 38 | ): 39 | """stream docker configs to root 40 | template: 41 | sage_painless/templates/Dockerfile.jinja 42 | sage_painless/templates/docker-compose.jinja 43 | """ 44 | start_time = time.time() 45 | 46 | diagram = self.load_json(diagram_path) 47 | if git_support: 48 | self.init_repo(settings.BASE_DIR) 49 | 50 | default_config = { 51 | "docker": { 52 | "db_image": "postgres", 53 | "db_name": "products", 54 | "db_user": "postgres", 55 | "db_pass": "postgres1234", 56 | "redis": False, 57 | "rabbitmq": False 58 | }, 59 | "gunicorn": { 60 | "reload": False 61 | }, 62 | "uwsgi": { 63 | "chdir": "/src/kernel", 64 | "home": "/src/venv", 65 | "module": "kernel.wsgi", 66 | "master": True, 67 | "pidfile": "/tmp/project-master.pid", 68 | "vacuum": False, 69 | "max-requests": 3000, 70 | "processes": 10, 71 | "daemonize": "/var/log/uwsgi/uwsgi.log" 72 | }, 73 | "package_manager": { 74 | "type": "pip" 75 | } 76 | } 77 | config = self.extract_deploy_config(diagram) # get deploy config from diagram 78 | default_config.update(config) # update default config with user input 79 | 80 | # package manager 81 | if package_manager_support: 82 | manager = default_config.get('package_manager').get('type') 83 | self.set_package_manager_type(manager) 84 | self.export_requirements() 85 | 86 | # stream to Dockerfile 87 | self.stream_to_template( 88 | output_path=f'{settings.BASE_DIR}/Dockerfile', 89 | template_path=os.path.abspath(templates.__file__).replace('__init__.py', self.DOCKERFILE_TEMPLATE), 90 | data={ 91 | 'gunicorn': gunicorn_support, 92 | 'kernel_name': self.get_kernel_name() 93 | } 94 | ) 95 | 96 | # stream to docker-compose.yml 97 | self.stream_to_template( 98 | output_path=f'{settings.BASE_DIR}/docker-compose.yml', 99 | template_path=os.path.abspath(templates.__file__).replace('__init__.py', self.DOCKER_COMPOSE_TEMPLATE), 100 | data={ 101 | 'kernel': self.get_kernel_name(), 102 | 'docker_config': default_config.get('docker', {}), 103 | 'gunicorn': gunicorn_support 104 | } 105 | ) 106 | 107 | # stream to .env.prod 108 | self.stream_to_template( 109 | output_path=f'{settings.BASE_DIR}/.env.prod', 110 | template_path=os.path.abspath(templates.__file__).replace('__init__.py', self.ENV_TEMPLATE), 111 | data={ 112 | 'config': default_config.get('docker', {}), 113 | 'random_postgres_password': get_random_secret_key(), 114 | 'random_rabbitmq_password': get_random_secret_key() 115 | } 116 | ) 117 | if git_support: 118 | self.commit_file( 119 | f'{settings.BASE_DIR}/Dockerfile', 120 | f'deploy (docker): Create Dockerfile' 121 | ) 122 | self.commit_file( 123 | f'{settings.BASE_DIR}/docker-compose.yml', 124 | f'deploy (docker): Create docker-compose.yml' 125 | ) 126 | self.commit_file( 127 | f'{settings.BASE_DIR}/.env.prod', 128 | f'deploy (docker): Add variables to .env' 129 | ) 130 | 131 | end_time = time.time() 132 | return True, 'Docker config generated ({:.3f} ms)'.format(self.calculate_execute_time(start_time, end_time)) 133 | -------------------------------------------------------------------------------- /tests/success/test_model_generator.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.test import TestCase 4 | from django.conf import settings 5 | 6 | from sage_painless.services.model_generator import ModelGenerator 7 | from sage_painless.utils.json_service import JsonHandler 8 | 9 | from sage_painless.classes.field import Field 10 | 11 | from tests import fixtures 12 | 13 | 14 | class TestModelGenerator(TestCase): 15 | def setUp(self) -> None: 16 | self.json_handler = JsonHandler() 17 | self.app_name = 'products' 18 | self.model_generator = ModelGenerator() 19 | self.diagram_path = os.path.abspath(fixtures.__file__).replace('__init__.py', 'product_fixture.json') 20 | self.diagram = self.json_handler.load_json(self.diagram_path).get('apps').get(self.app_name).get('models') 21 | self.field = Field() 22 | self.field_types = self.field.field_types 23 | 24 | def get_table_names(self): 25 | return self.diagram.keys() 26 | 27 | def get_table_signals(self): 28 | signals = list() 29 | for table_name in self.diagram.keys(): 30 | table = self.diagram.get(table_name) 31 | fields = self.model_generator.get_table_fields(table) 32 | for field_name in fields.keys(): 33 | field_data = fields.get(field_name) 34 | for key in field_data.keys(): 35 | if key == self.model_generator.TYPE_KEYWORD: 36 | if field_data.get(self.model_generator.TYPE_KEYWORD) == 'one2one': 37 | signals.append(field_name) 38 | return signals 39 | 40 | def get_table_field_names(self, table_name): 41 | return self.diagram.get(table_name).get('fields').keys() 42 | 43 | def get_field_type(self, table_name, field_name): 44 | return self.diagram.get(table_name).get('fields').get(field_name).get('type') 45 | 46 | def get_field_attr_names(self, table_name, field_name): 47 | attrs = list(self.diagram.get(table_name).get('fields').get(field_name).keys()) 48 | attrs.remove('type') 49 | return attrs 50 | 51 | def get_attr_value(self, table_name, field_name, attr_name): 52 | return self.diagram.get(table_name).get('fields').get(field_name).get(attr_name) 53 | 54 | def check_validator_support(self, models): 55 | for model in models: 56 | for field in model.fields: 57 | if len(field.validators) > 0: 58 | return True 59 | 60 | return False 61 | 62 | def check_signal_support(self, models): 63 | for model in models: 64 | for field in model.fields: 65 | if field.type == 'OneToOneField': 66 | return True 67 | 68 | return False 69 | 70 | def test_extract_models(self): 71 | """ 72 | extract models from Json diagram 73 | """ 74 | models, signals = self.model_generator.extract_models_and_signals(self.diagram) 75 | 76 | self.assertEqual(len(models), len(self.get_table_names())) 77 | 78 | for model in models: 79 | self.assertIn(model.name, self.get_table_names()) 80 | for field in model.fields: 81 | self.assertIn( 82 | field.name, self.get_table_field_names(model.name) 83 | ) 84 | self.assertEqual( 85 | field.type, self.field_types.get( 86 | self.get_field_type( 87 | model.name, 88 | field.name 89 | ) 90 | ).get('type') 91 | ) 92 | self.assertEqual( 93 | len(field.attrs), len(self.get_field_attr_names( 94 | model.name, 95 | field.name 96 | )) 97 | ) 98 | for attribute in field.attrs: 99 | self.assertIn( 100 | attribute.key, self.get_field_attr_names( 101 | model.name, 102 | field.name 103 | ) 104 | ) 105 | self.assertEqual( 106 | attribute.value, self.get_attr_value( 107 | model.name, 108 | field.name, 109 | attribute.key 110 | ) 111 | ) 112 | 113 | self.assertEqual(len(signals), len(self.get_table_signals())) 114 | 115 | for signal in signals: 116 | self.assertIn(signal.field, self.get_table_signals()) 117 | self.assertIn(signal.model_a, self.get_table_names()) 118 | self.assertEqual(signal.model_b, self.get_attr_value(signal.model_a, signal.field, 'to')) 119 | 120 | def test_check_validator_support(self): 121 | """ 122 | check is there validator in models 123 | """ 124 | models, signals = self.model_generator.extract_models_and_signals(self.diagram) 125 | check_model_generator = self.model_generator.check_validator_support(models) 126 | check_test = self.check_validator_support(models) 127 | self.assertEqual(check_test, check_model_generator) 128 | 129 | def test_check_signal_support(self): 130 | """ 131 | check need signal in models 132 | """ 133 | models, signals = self.model_generator.extract_models_and_signals(self.diagram) 134 | check_model_generator = self.model_generator.check_signal_support(models) 135 | check_test = self.check_signal_support(models) 136 | self.assertEqual(check_test, check_model_generator) 137 | -------------------------------------------------------------------------------- /sage_painless/management/commands/deploy.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - deploy management command 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | import datetime 7 | 8 | from django.core.management import BaseCommand 9 | 10 | from sage_painless.services.docker_generator import DockerGenerator 11 | from sage_painless.services.gunicorn_generator import GunicornGenerator 12 | from sage_painless.services.nginx_generator import NginxGenerator 13 | from sage_painless.services.tox_generator import ToxGenerator 14 | from sage_painless.services.uwsgi_generator import UwsgiGenerator 15 | from sage_painless.utils.report_service import ReportUserAnswer 16 | 17 | 18 | class Command(BaseCommand): 19 | help = 'Generate all files need to deploy project.' 20 | 21 | def add_arguments(self, parser): 22 | """initialize arguments""" 23 | parser.add_argument( 24 | '-d', '--diagram', type=str, help='sql diagram path that will generate from it', required=True) 25 | parser.add_argument('-g', '--git', type=bool, help='generate git commits', required=False) 26 | 27 | def handle(self, *args, **options): 28 | diagram_path = options.get('diagram') 29 | git_support = options.get('git', False) 30 | stdout_messages = list() # initial empty messages 31 | 32 | reporter = ReportUserAnswer( 33 | app_name='deploy-config', 34 | file_prefix=f'deploy-config-{int(datetime.datetime.now().timestamp())}' 35 | ) 36 | reporter.init_report_file() 37 | 38 | # generate gunicorn config 39 | gunicorn_support = input('Would you like to generate gunicorn config(yes/no)? ') 40 | gunicorn_support = True if gunicorn_support == 'yes' else False 41 | 42 | if gunicorn_support: 43 | reporter.add_question_answer( 44 | question='create gunicorn conf.py', 45 | answer=True 46 | ) 47 | gunicorn_generator = GunicornGenerator() 48 | check, message = gunicorn_generator.generate(diagram_path, git_support=git_support) 49 | if check: 50 | stdout_messages.append(self.style.SUCCESS(f'deploy[INFO]: {message}')) 51 | else: 52 | stdout_messages.append(self.style.ERROR(f'deploy[ERROR]: {message}')) 53 | else: 54 | reporter.add_question_answer( 55 | question='create gunicorn conf.py', 56 | answer=False 57 | ) 58 | 59 | # generate uwsgi config 60 | uwsgi_support = input('Would you like to generate uwsgi config(yes/no)? ') 61 | uwsgi_support = True if uwsgi_support == 'yes' else False 62 | 63 | if uwsgi_support: 64 | reporter.add_question_answer( 65 | question='create uwsgi.ini', 66 | answer=True 67 | ) 68 | uwsgi_generator = UwsgiGenerator() 69 | check, message = uwsgi_generator.generate(diagram_path, git_support=git_support) 70 | if check: 71 | stdout_messages.append(self.style.SUCCESS(f'deploy[INFO]: {message}')) 72 | else: 73 | stdout_messages.append(self.style.ERROR(f'deploy[ERROR]: {message}')) 74 | else: 75 | reporter.add_question_answer( 76 | question='create uwsgi.ini', 77 | answer=False 78 | ) 79 | 80 | # generate nginx config 81 | nginx_support = input('Would you like to generate nginx config(yes/no)? ') 82 | nginx_support = True if nginx_support == 'yes' else False 83 | 84 | if nginx_support: 85 | reporter.add_question_answer( 86 | question='create nginx.conf', 87 | answer=True 88 | ) 89 | nginx_generator = NginxGenerator() 90 | check, message = nginx_generator.generate(git_support=git_support) 91 | if check: 92 | stdout_messages.append(self.style.SUCCESS(f'deploy[INFO]: {message}')) 93 | else: 94 | stdout_messages.append(self.style.ERROR(f'deploy[ERROR]: {message}')) 95 | else: 96 | reporter.add_question_answer( 97 | question='create nginx.conf', 98 | answer=False 99 | ) 100 | 101 | # package manager support 102 | package_manager_support = input('Would you like to export project requirement packages(yes/no)? ') 103 | package_manager_support = True if package_manager_support == 'yes' else False 104 | 105 | # generate docker config 106 | docker_support = input('Would you like to dockerize your project(yes/no)? ') 107 | docker_support = True if docker_support == 'yes' else False 108 | 109 | if docker_support: 110 | reporter.add_question_answer( 111 | question='create docker-compose.yml', 112 | answer=True 113 | ) 114 | reporter.add_question_answer( 115 | question='create Dockerfile', 116 | answer=True 117 | ) 118 | 119 | docker_generator = DockerGenerator() 120 | check, message = docker_generator.generate( 121 | diagram_path, 122 | gunicorn_support=gunicorn_support, 123 | # uwsgi_support=uwsgi_support, # TODO: will support in future 124 | # nginx_support=nginx_support, # TODO: will support in future 125 | git_support=git_support, 126 | package_manager_support=package_manager_support 127 | ) 128 | 129 | if check: 130 | stdout_messages.append(self.style.SUCCESS(f'deploy[INFO]: {message}')) 131 | else: 132 | stdout_messages.append(self.style.ERROR(f'deploy[ERROR]: {message}')) 133 | 134 | else: 135 | reporter.add_question_answer( 136 | question='create docker-compose.yml', 137 | answer=False 138 | ) 139 | reporter.add_question_answer( 140 | question='create Dockerfile', 141 | answer=False 142 | ) 143 | 144 | # generate tox config 145 | tox_support = input('Would you like to generate tox & coverage config files(yes/no)? ') 146 | tox_support = True if tox_support == 'yes' else False 147 | 148 | reporter = ReportUserAnswer( 149 | app_name='tox', 150 | file_prefix=f'tox-{int(datetime.datetime.now().timestamp())}' 151 | ) 152 | reporter.init_report_file() 153 | 154 | if tox_support: 155 | reporter.add_question_answer( 156 | question='create tox & coverage config', 157 | answer=True 158 | ) 159 | tox_generator = ToxGenerator() 160 | check, message = tox_generator.generate(diagram_path, git_support=git_support) 161 | if check: 162 | stdout_messages.append(self.style.SUCCESS(f'deploy[INFO]: {message}')) 163 | else: 164 | stdout_messages.append(self.style.ERROR(f'deploy[ERROR]: {message}')) 165 | else: 166 | reporter.add_question_answer( 167 | question='create tox & coverage config', 168 | answer=False 169 | ) 170 | 171 | # print messages in terminal 172 | for message in stdout_messages: 173 | self.stdout.write(message) 174 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django Sage Painless 2 | ==================== 3 | 4 | django-sage-painless is a useful package based on Django Web Framework & Django Rest Framework for high-level and rapid web development. 5 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 6 | 7 | |SageTeam| 8 | 9 | |PyPI release| |Supported Python versions| |Supported Django 10 | versions| |Documentation| |Build| 11 | 12 | - `Project Detail <#project-detail>`__ 13 | - `Git Rules <#git-rules>`__ 14 | - `Get Started <#getting-started>`__ 15 | - `Usage <#usage>`__ 16 | - `Contribute <#contribute>`__ 17 | 18 | Project Detail 19 | -------------- 20 | 21 | \* Frameworks: - Django > 3.1 \* Language: Python > 3.6 22 | 23 | Git Rules 24 | --------- 25 | 26 | Sage team Git Rules Policy is available here: 27 | 28 | - `Sage Git 29 | Policy `__ 30 | 31 | Getting Started 32 | --------------- 33 | 34 | Before creating django project you must first create virtualenv. 35 | 36 | .. code:: shell 37 | 38 | $ python3.9 -m pip install virtualenv 39 | $ python3.9 -m virtualenv venv 40 | 41 | To activate virtualenvironment in ubuntu: 42 | 43 | .. code:: shell 44 | 45 | $ source venv/bin/activate 46 | 47 | To deactive vritualenvironment use: 48 | 49 | .. code:: shell 50 | 51 | $ deactivate 52 | 53 | Start Project 54 | ------------- 55 | 56 | First create a Django project 57 | 58 | .. code:: shell 59 | 60 | $ mkdir GeneratorTutorials 61 | $ cd GeneratorTutorials 62 | $ django-admin startproject kernel . 63 | 64 | Next we have to create an sample app that we want to generate code for 65 | it (it is required for development. you will run tests on this app) 66 | 67 | .. code:: shell 68 | 69 | $ python manage.py startapp products 70 | 71 | Now we have to add 'products' to INSTALLED\_APPS in settings.py 72 | 73 | .. code:: python 74 | 75 | INSTALLED_APPS = [ 76 | 'products', 77 | ] 78 | 79 | Install Generator 80 | ----------------- 81 | 82 | First install package 83 | 84 | .. code:: shell 85 | 86 | $ pip install django-sage-painless 87 | 88 | Then add 'sage\_painless' to INSTALLED\_APPS in settings.py 89 | 90 | These apps should be in your INSTALLED\_APPS: 91 | 92 | - 'rest\_framework' 93 | - 'drf\_yasg' 94 | - 'django\_seed' 95 | 96 | .. code:: python 97 | 98 | INSTALLED_APPS = [ 99 | 'sage_painless', 100 | 'rest_framework', 101 | 'drf_yasg', 102 | 'django_seed', 103 | ] 104 | 105 | Usage 106 | ----- 107 | 108 | For generating a whole project you just need a diagram. diagram is a 109 | json file that contains information about database tables. 110 | 111 | `you can find examples of diagram file 112 | here `__ 113 | 114 | start to generate (it is required for development. you will run tests on 115 | this app) 116 | 117 | First validate the format of your diagram 118 | 119 | .. code:: shell 120 | 121 | $ python manage.py validate_diagram --diagram 122 | 123 | Now you can generate code 124 | 125 | .. code:: shell 126 | 127 | $ python manage.py generate --diagram 128 | 129 | Here system will ask you what you want to generate for your app. 130 | 131 | If you generated api you have to add app urls to urls.py: 132 | 133 | .. code:: python 134 | 135 | urlpatterns = [ 136 | path('api/', include('products.api.urls')), 137 | ] 138 | 139 | - You have to migrate your new models 140 | 141 | .. code:: shell 142 | 143 | $ python manage.py makemigrations 144 | $ python manage.py migrate 145 | 146 | - You can run tests for your app 147 | 148 | .. code:: shell 149 | 150 | $ python manage.py test products 151 | 152 | - Django run server 153 | 154 | .. code:: shell 155 | 156 | $ python manage.py runserver 157 | 158 | - Rest API documentation is available at ``localhost:8000/api/doc/`` 159 | 160 | - For support Rest API doc add this part to your urls.py 161 | 162 | .. code:: python 163 | 164 | from rest_framework.permissions import AllowAny 165 | from drf_yasg.views import get_schema_view 166 | from drf_yasg import openapi 167 | 168 | schema_view = get_schema_view( 169 | openapi.Info( 170 | title="Rest API Doc", 171 | default_version='v1', 172 | description="Auto Generated API Docs", 173 | license=openapi.License(name="S.A.G.E License"), 174 | ), 175 | public=True, 176 | permission_classes=(AllowAny,), 177 | ) 178 | 179 | urlpatterns = [ 180 | path('api/doc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-swagger-ui'), 181 | ] 182 | 183 | - Rest API documentation is available at ``localhost:8000/api/doc/`` 184 | 185 | Contribute 186 | ---------- 187 | 188 | Run project tests before starting to develop 189 | 190 | - ``products`` app is required for running tests 191 | 192 | .. code:: shell 193 | 194 | $ python manage.py startapp products 195 | 196 | .. code:: python 197 | 198 | INSTALLED_APPS = [ 199 | 'products', 200 | ] 201 | 202 | - you have to generate everything for this app 203 | 204 | - diagram file is available here: 205 | `Diagram `__ 206 | 207 | .. code:: shell 208 | 209 | $ python manage.py generate --diagram sage_painless/tests/diagrams/product_diagram.json 210 | 211 | - run tests 212 | 213 | .. code:: shell 214 | 215 | $ python manage.py test sage_painless 216 | 217 | Team 218 | ---- 219 | 220 | +-----------------------------------------------------------------+---------------------------------------------------------+ 221 | | |sepehr| | |mehran| | 222 | +=================================================================+=========================================================+ 223 | | `Sepehr Akbarazadeh `__ | `Mehran Rahmanzadeh `__ | 224 | +-----------------------------------------------------------------+---------------------------------------------------------+ 225 | 226 | .. |SageTeam| image:: https://github.com/sageteam-org/django-sage-painless/blob/develop/docs/images/tag_sage.png?raw=true 227 | :alt: SageTeam 228 | .. |PyPI release| image:: https://img.shields.io/pypi/v/django-sage-painless 229 | :alt: django-sage-painless 230 | .. |Supported Python versions| image:: https://img.shields.io/pypi/pyversions/django-sage-painless 231 | :alt: django-sage-painless 232 | .. |Supported Django versions| image:: https://img.shields.io/pypi/djversions/django-sage-painless 233 | :alt: django-sage-painless 234 | .. |Documentation| image:: https://img.shields.io/readthedocs/django-sage-painless 235 | :alt: django-sage-painless 236 | .. |Build| image:: https://img.shields.io/appveyor/build/mrhnz/django-sage-painless 237 | :alt: django-sage-painless 238 | .. |sepehr| image:: https://github.com/sageteam-org/django-sage-painless/blob/develop/docs/images/sepehr.jpeg?raw=true 239 | :height: 230px 240 | :width: 230px 241 | :alt: Sepehr Akbarzadeh 242 | .. |mehran| image:: https://github.com/sageteam-org/django-sage-painless/blob/develop/docs/images/mehran.png?raw=true 243 | :height: 340px 244 | :width: 225px 245 | :alt: Mehran Rahmanzadeh 246 | -------------------------------------------------------------------------------- /sage_painless/utils/comment_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - Comments Main Observer Class 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | 7 | 8 | class CommentService: 9 | # help comments using in gunicorn-conf.py template 10 | GUNICORN_CONFIG_COMMENTS = { 11 | 'bind': 'bind - The server socket to bind', 12 | 'backlog': 'backlog - The maximum number of pending connections (Generally in range 64-2048)', 13 | 'workers': 'workers - The number of worker processes for handling requests (A positive integer generally in ' 14 | 'the 2-4 x $(NUM_CORES) range)', 15 | 'worker_class': 'worker_class - The type of workers to use. A string referring to one of the following ' 16 | 'bundled classes 1. sync 2. eventlet - Requires eventlet >= 0.9.7 3. gevent - Requires ' 17 | 'gevent >= 0.13 4. tornado - Requires tornado >= 0.2 5. gthread - Python 2 requires the ' 18 | 'futures package to be installed 6. uvicorn - uvicorn.workers.UvicornWorker ', 19 | 'threads': 'threads - The number of worker threads for handling requests. This will run each worker with the ' 20 | 'specified number of threads. A positive integer generally in the 2-4 x $(NUM_CORES) range ', 21 | 'worker_connections': 'worker_connections - The maximum number of simultaneous clients.This setting only ' 22 | 'affects the Eventlet and Gevent worker types.', 23 | 'max_requests': 'max_requests - The maximum number of requests a worker will process. Any value greater than ' 24 | 'zero will limit the number of requests a work will process before automatically restarting. ' 25 | 'This is a simple method to help limit the damage of memory leaks ', 26 | 'max_requests_jitter': 'max_requests_jitter - The maximum jitter to add to the max-requests setting', 27 | 'timeout': 'timeout - Workers silent for more than this many seconds are killed and restarted', 28 | 'graceful_timeout': 'graceful_timeout - Timeout for graceful workers restart. How max time worker can handle ' 29 | 'request after got restart signal. If the time is up worker will be force killed. ', 30 | 'keep_alive': 'keep_alive - The number of seconds to wait for requests on a Keep-Alive connection (Generally ' 31 | 'set in the 1-5 seconds range.) ', 32 | 'limit_request_line': 'limit_request_line - The maximum size of HTTP request line in bytes. Value is a number ' 33 | 'from 0 (unlimited) to 8190. This parameter can be used to prevent any DDOS attack. ', 34 | 'limit_request_fields': 'limit_request_fields - Limit the number of HTTP headers fields in a request. This ' 35 | 'parameter is used to limit the number of headers in a request to prevent DDOS ' 36 | 'attack. Used with the limit_request_field_size it allows more safety. By default ' 37 | 'this value is 100 and can’t be larger than 32768. ', 38 | 'reload': 'reload - Restart workers when code changes', 39 | 'reload_engine': 'reload_engine - The implementation that should be used to power reload.', 40 | 'reload_extra_files': 'reload_extra_files - Extends reload option to also watch and reload on additional ' 41 | 'files (e.g., templates, configurations, specifications, etc.) ', 42 | 'spew': 'spew - Install a trace function that spews every line executed by the server', 43 | 'check_config': 'check_config - Check the configuration', 44 | 'preload_app': 'preload_app - Load application code before the worker processes are forked', 45 | 'sendfile': 'sendfile - Enables or disables the use of sendfile()', 46 | 'reuse_port': 'reuse_port - Set the SO_REUSEPORT flag on the listening socket.', 47 | 'chdir': 'chdir - Chdir to specified directory before apps loading', 48 | 'daemon': 'daemon - Daemonize the Gunicorn process.', 49 | 'raw_env': 'raw_env - Set environment variable (key=value)', 50 | 'pidfile': 'pidfile - A filename to use for the PID file. If not set, no PID file will be written.', 51 | 'worker_tmp_dir': 'worker_tmp_dir - A directory to use for the worker heartbeat temporary file. If not set, ' 52 | 'the default temporary directory will be used.', 53 | 'user': 'user - Switch worker processes to run as this user. A valid user id (as an integer) or the name of a ' 54 | 'user that can be retrieved.', 55 | 'group': 'group - Switch worker process to run as this group.', 56 | 'umask': 'umask - A bit mask for the file mode on files written by Gunicorn. Note that this affects unix ' 57 | 'socket permissions.', 58 | 'initgroups': 'initgroups - If true, set the worker process’s group access list with all of the groups of ' 59 | 'which the specified username is a member, plus the specified group id.', 60 | 'tmp_upload_dir': 'tmp_upload_dir - Directory to store temporary request data as they are read. This path ' 61 | 'should be writable by the process permissions set for Gunicorn.', 62 | 'secure_scheme_headers': 'secure_scheme_headers - A dictionary containing headers and values that the ' 63 | 'front-end proxy uses to indicate HTTPS requests. These tell gunicorn to set ' 64 | 'wsgi.url_scheme to “https”, so your application can tell that the request is ' 65 | 'secure.', 66 | 'forwarded_allow_ips': 'forwarded_allow_ips - Front-end’s IPs from which allowed to handle set secure headers ' 67 | '(comma separate).', 68 | 'pythonpath': 'pythonpath - A comma-separated list of directories to add to the Python path.', 69 | 'paste': 'paste - Load a PasteDeploy config file.', 70 | 'proxy_protocol': 'proxy_protocol - Enable detect PROXY protocol (PROXY mode).', 71 | 'proxy_allow_ips': 'proxy_allow_ips - Front-end’s IPs from which allowed accept proxy requests (comma ' 72 | 'separate).', 73 | 'accesslog': 'accesslog - The Access log file to write to. “-” means log to stdout.', 74 | 'access_log_format': 'access_log_format - The access log format.', 75 | 'disable_redirect_access_to_syslog': 'disable_redirect_access_to_syslog - Disable redirect access logs to ' 76 | 'syslog.', 77 | 'errorlog': 'errorlog - The Error log file to write to. “-” means log to stderr.', 78 | 'loglevel': 'loglevel - The granularity of Error log outputs. Valid level names are: 1. debug 2. info 3. ' 79 | 'warning 4. error 5. critical', 80 | 'capture_output': 'capture_output - Redirect stdout/stderr to specified file in errorlog.', 81 | 'logger_class': 'logger_class - The logger you want to use to log events in gunicorn.', 82 | 'logconfig': 'logconfig - The log config file to use. Gunicorn uses the standard Python logging module’s ' 83 | 'Configuration file format.', 84 | 'logconfig_dict': 'logconfig_dict - The log config dictionary to use, using the standard Python logging ' 85 | 'module’s dictionary configuration format.', 86 | 'proc_name': 'proc_name - A base to use with setproctitle for process naming.' 87 | } 88 | 89 | def __init__(self, *args, **kwargs): 90 | super().__init__(*args, **kwargs) 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Sage Painless 2 | 3 | The [django-sage-painless](https://github.com/sageteam-org/django-sage-painless) is a valuable package based on Django Web Framework & Django Rest Framework for high-level and rapid web development. The introduced package generates Django applications. After completing many projects, we concluded that any basic project and essential part is its database structure. You can give the database schema in this package and get some parts of the Django application, such as API, models, admin, signals, model cache, setting configuration, mixins, etc. All of these capabilities come with a unit test. So you no longer have to worry about the simple parts of Django, and now you can write your advanced services in Django. The django-sage-painless dramatically speeds up the initial development of your projects. Documentation of this package is available in [readthedocs](https://django-sage-painless.readthedocs.io/). 4 | 5 | ## Vision 6 | 7 | However, we intend to make it possible to use it in projects that are in-progress. 8 | 9 | ## Why Painless 10 | 11 | We used the name painless instead of the Django code generator because this package allows you to reach your goals with less effort. 12 | 13 |   14 | 15 | [![SageTeam](https://github.com/sageteam-org/django-sage-painless/blob/develop/docs/images/tag_sage.png?raw=true "SageTeam")](http://sageteam.org) 16 | 17 | ![License](https://img.shields.io/github/license/sageteam-org/django-sage-painless "django-sage-painless") 18 | ![PyPI release](https://img.shields.io/pypi/v/django-sage-painless "django-sage-painless") 19 | ![Supported Python versions](https://img.shields.io/pypi/pyversions/django-sage-painless "django-sage-painless") 20 | ![Supported Django versions](https://img.shields.io/pypi/djversions/django-sage-painless "django-sage-painless") 21 | ![Documentation](https://img.shields.io/readthedocs/django-sage-painless "django-sage-painless") 22 | ![Build](https://img.shields.io/appveyor/build/mrhnz/django-sage-painless "django-sage-painless") 23 | ![Last Commit](https://img.shields.io/github/last-commit/sageteam-org/django-sage-painless/develop "django-sage-painless") 24 | ![Languages](https://img.shields.io/github/languages/top/sageteam-org/django-sage-painless "django-sage-painless") 25 | ![Downloads](https://static.pepy.tech/badge/django-sage-painless "django-sage-painless") 26 | 27 | - [Project Detail](#project-detail) 28 | - [Get Started](#getting-started) 29 | - [Usage](#usage) 30 | - [Contribute](#how-to-contribute) 31 | - [Git Rules](#git-rules) 32 | 33 | ## Project Detail 34 | 35 | - Language: Python > 3.6 36 | - Framework: Django > 3.1 37 | 38 | ## Getting Started 39 | 40 | Before creating Djagno project you must first create virtualenv. 41 | 42 | ``` shell 43 | $ python3.9 -m pip install virtualenv 44 | $ python3.9 -m virtualenv venv 45 | ``` 46 | 47 | To activate virtualenvironment in ubuntu: 48 | 49 | ```shell 50 | $ source venv/bin/activate 51 | ``` 52 | 53 | To deactive vritualenvironment use: 54 | 55 | ``` shell 56 | $ deactivate 57 | ``` 58 | 59 | ## Start Project 60 | 61 | First create a Django project 62 | 63 | ```shell 64 | $ mkdir GeneratorTutorials 65 | $ cd GeneratorTutorials 66 | $ django-admin startproject kernel . 67 | ``` 68 | 69 | ## Install Generator 70 | 71 | First install package 72 | 73 | ```shell 74 | $ pip install django-sage-painless 75 | ``` 76 | 77 | Then add 'sage_painless' to INSTALLED_APPS in settings.py 78 | 79 | **TIP:** You do not need to install the following packages unless you request to automatically generate an API or API documentation. 80 | 81 | However, you can add following apps in your INSTALLED_APPS: 82 | 83 | - rest_framework 84 | - drf_yasg 85 | - django_seed 86 | 87 | ```python 88 | INSTALLED_APPS = [ 89 | 'sage_painless', 90 | 'rest_framework', 91 | 'drf_yasg', 92 | 'django_seed', 93 | ] 94 | ``` 95 | 96 | ## Usage 97 | 98 | To generate a Django app you just need a diagram in JSON format. diagram is a json file that contains information about database tables. 99 | 100 | [Diagram examples](sage_painless/docs/diagrams) 101 | 102 | start to generate 103 | (it is required for development. you will run tests on this app) 104 | 105 | - First validate your diagram format. It will raise errors if your diagram format is incorrect. 106 | 107 | ```shell 108 | $ python manage.py validate_diagram --diagram 109 | ``` 110 | 111 | - Now you can generate code (you need generate diagram json file) 112 | 113 | [Generate diagram sample](tests/diagrams/product_diagram.json) 114 | 115 | ```shell 116 | $ python manage.py generate --diagram 117 | ``` 118 | 119 | - You can generate deploy config files (you need a deploy diagram json file) 120 | 121 | [Deploy diagram sample](tests/diagrams/deploy_diagram.json) 122 | 123 | ```shell 124 | $ python manage.py deploy --diagram 125 | ``` 126 | 127 | - You can generate doc files (README, etc) 128 | 129 | ```shell 130 | $ python manage.py docs --diagram 131 | ``` 132 | 133 | Here system will ask you what you want to generate for your app. 134 | 135 | - If you generated api you have to add app urls to main urls.py: 136 | 137 | ```python 138 | urlpatterns = [ 139 | path('api/', include('products.api.urls')), 140 | ] 141 | ``` 142 | 143 | - You have to migrate your new models 144 | 145 | ```shell 146 | $ python manage.py makemigrations 147 | $ python manage.py migrate 148 | ``` 149 | 150 | - You can run tests for your app 151 | 152 | ```shell 153 | $ python manage.py test products 154 | ``` 155 | 156 | - Django run server 157 | 158 | ```shell 159 | $ python manage.py runserver 160 | ``` 161 | 162 | - For support Rest API doc add this part to your urls.py 163 | 164 | ```python 165 | from rest_framework.permissions import AllowAny 166 | from drf_yasg.views import get_schema_view 167 | from drf_yasg import openapi 168 | 169 | schema_view = get_schema_view( 170 | openapi.Info( 171 | title="Rest API Doc", 172 | default_version='v1', 173 | description="Auto Generated API Docs", 174 | license=openapi.License(name="S.A.G.E License"), 175 | ), 176 | public=True, 177 | permission_classes=(AllowAny,), 178 | ) 179 | 180 | urlpatterns = [ 181 | path('api/doc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-swagger-ui'), 182 | ] 183 | ``` 184 | 185 | - Rest API documentation is available at `localhost:8000/api/doc/` 186 | 187 | ## How to Contribute 188 | 189 | Run project tests before starting to develop 190 | 191 | - `products` app is required for running tests 192 | 193 | ```shell 194 | $ python manage.py startapp products 195 | ``` 196 | 197 | ```python 198 | INSTALLED_APPS = [ 199 | 'products', 200 | ] 201 | ``` 202 | 203 | - you have to generate everything for this app 204 | 205 | - diagram file is available here: [Diagram](tests/diagrams/product_diagram.json) 206 | - download diagram file and generate test app using this commend 207 | 208 | ```shell 209 | $ python manage.py generate --diagram tests/diagrams/product_diagram.json 210 | ``` 211 | 212 | - run tests 213 | 214 | ```shell 215 | $ python manage.py test sage_painless 216 | ``` 217 | 218 | ## Git Rules 219 | 220 | S.A.G.E. team Git Rules Policy is available here: 221 | 222 | - [S.A.G.E. Git Policy](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) 223 | 224 | ## Version 1 225 | - [x] generate README.md 226 | - [x] db encryption 227 | - [x] video streaming 228 | - [x] improve test generation 229 | - [x] coverage & tox 230 | - [x] deployment questionnaire 231 | - [x] management command 232 | - [x] docker 233 | - [x] gunicorn, uwsgi, etc 234 | - [x] nginx configuration 235 | - [x] commit generation 236 | - [ ] GitHub repo integration 237 | - [ ] CI CD 238 | - [ ] Database Support (postgres, mysql, sqlite, sql server) 239 | - [ ] security config and check 240 | - [ ] seo 241 | - [ ] graphql 242 | - [x] package manager support 243 | 244 | ## Version 2 245 | - [ ] Django admin generator 246 | - [ ] modular setting 247 | - [ ] mock data generator 248 | - [ ] file field svg support 249 | - [ ] image field enhancement 250 | - [ ] traditional mixins 251 | - [ ] decorator signals 252 | - [ ] logging configuration 253 | - [ ] sentry support 254 | - [ ] monitoring feature (prometheus) 255 | - [ ] track data history 256 | - [ ] stream upload 257 | - [ ] CORS config 258 | - [ ] mongo support 259 | - [ ] Elastic search configuration 260 | - [ ] cache support (+Memcache) 261 | - [ ] error handlers (2xx, 3xx, 4xx, 5xx) 262 | - [ ] django forms support 263 | - [ ] django view support 264 | - [ ] image compression support 265 | - [ ] debug tools support 266 | -------------------------------------------------------------------------------- /sage_painless/management/commands/generate.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-sage-painless - generate management command 3 | 4 | :author: Mehran Rahmanzadeh (mrhnz13@gmail.com) 5 | """ 6 | import datetime 7 | 8 | from django.core.management.base import BaseCommand 9 | from django.conf import settings 10 | 11 | from sage_painless.services.model_generator import ModelGenerator 12 | from sage_painless.services.admin_generator import AdminGenerator 13 | from sage_painless.services.api_generator import APIGenerator 14 | from sage_painless.services.test_generator import TestGenerator 15 | from sage_painless.utils.json_service import JsonHandler 16 | from sage_painless.utils.report_service import ReportUserAnswer 17 | from sage_painless.validators.diagram_validator import DiagramValidator 18 | 19 | 20 | class Command(BaseCommand, JsonHandler, DiagramValidator): 21 | APPS_KEYWORD = 'apps' 22 | help = 'Generate all files need to your new apps.' 23 | redis_message_showed = False 24 | 25 | def add_arguments(self, parser): 26 | """initialize arguments""" 27 | parser.add_argument( 28 | '-d', '--diagram', type=str, help='sql diagram path that will make models.py from it', required=True) 29 | parser.add_argument('-g', '--git', type=bool, help='generate git commits', required=False) 30 | 31 | @classmethod 32 | def validate_settings(cls, step): 33 | """validate required settings in each step""" 34 | if 'sage_painless' not in settings.INSTALLED_APPS: 35 | raise IOError('Add `sage_painless` to your INSTALLED_APPS') 36 | 37 | if step == 'api': 38 | if 'rest_framework' not in settings.INSTALLED_APPS: 39 | raise IOError('Add `rest_framework` to your INSTALLED_APPS') 40 | 41 | if step == 'test': 42 | if 'rest_framework' not in settings.INSTALLED_APPS: 43 | raise IOError('Add `rest_framework` to your INSTALLED_APPS') 44 | if 'django_seed' not in settings.INSTALLED_APPS: 45 | raise IOError('Add `django_seed` to your INSTALLED_APPS') 46 | 47 | if step == 'docs': 48 | if 'drf_yasg' not in settings.INSTALLED_APPS: 49 | raise IOError('Add `drf_yasg` to your INSTALLED_APPS') 50 | 51 | def handle(self, *args, **options): 52 | """get configs from user and generate""" 53 | diagram_path = options.get('diagram') 54 | git_support = options.get('git', False) 55 | diagram = self.load_json(diagram_path) 56 | self.validate_all(diagram) # validate diagram 57 | stdout_messages = list() 58 | for app_name in diagram.get(self.APPS_KEYWORD).keys(): 59 | create_model = input(f'Would you like to generate models.py for {app_name} app (yes/no)? ') 60 | create_admin = input(f'Would you like to generate admin.py for {app_name} app (yes/no)? ') 61 | create_api = input(f'Would you like to generate serializers.py & views.py for {app_name} app (yes/no)? ') 62 | create_test = input(f'Would you like to generate test for {app_name} app (yes/no)? ') 63 | cache_support = input(f'Would you like to add cache queryset support for {app_name} app (yes/no)? ') 64 | 65 | reporter = ReportUserAnswer( 66 | app_name=app_name, 67 | file_prefix=f'{app_name}-{int(datetime.datetime.now().timestamp())}' 68 | ) 69 | reporter.init_report_file() 70 | 71 | if create_model == 'yes': 72 | model_generator = ModelGenerator() 73 | reporter.add_question_answer( 74 | question='create models.py', 75 | answer=True 76 | ) 77 | if cache_support == 'yes': 78 | reporter.add_question_answer( 79 | question='cache support', 80 | answer=True 81 | ) 82 | check, message = model_generator.generate(diagram_path, app_name, True, git_support) 83 | else: 84 | reporter.add_question_answer( 85 | question='cache support', 86 | answer=False 87 | ) 88 | check, message = model_generator.generate(diagram_path, app_name, git_support=git_support) 89 | 90 | if check: 91 | stdout_messages.append(self.style.SUCCESS(f'{app_name}[INFO]: {message}')) 92 | else: 93 | stdout_messages.append(self.style.ERROR(f'{app_name}[ERROR]: {message}')) 94 | else: 95 | reporter.add_question_answer( 96 | question='create models.py', 97 | answer=False 98 | ) 99 | 100 | if create_admin == 'yes': 101 | reporter.add_question_answer( 102 | question='create admin.py', 103 | answer=True 104 | ) 105 | admin_generator = AdminGenerator() 106 | check, message = admin_generator.generate(diagram_path, app_name, git_support=git_support) 107 | if check: 108 | stdout_messages.append(self.style.SUCCESS(f'{app_name}[INFO]: {message}')) 109 | else: 110 | stdout_messages.append(self.style.ERROR(f'{app_name}[ERROR]: {message}')) 111 | else: 112 | reporter.add_question_answer( 113 | question='create admin.py', 114 | answer=False 115 | ) 116 | 117 | if create_api == 'yes': 118 | reporter.add_question_answer( 119 | question='create serializers.py', 120 | answer=True 121 | ) 122 | reporter.add_question_answer( 123 | question='create views.py', 124 | answer=True 125 | ) 126 | self.validate_settings(step='api') 127 | api_generator = APIGenerator() 128 | 129 | if cache_support == 'yes': 130 | if not self.redis_message_showed: 131 | self.redis_message_showed = True 132 | print(""" 133 | hint: Redis setting should be in settings.py 134 | 135 | REDIS_URL = 'redis://localhost:6379/' 136 | CACHES = { 137 | "default": { 138 | "BACKEND": "django_redis.cache.RedisCache", 139 | "LOCATION": os.environ['REDIS_URL'] if os.environ.get('REDIS_URL') else settings.REDIS_URL if hasattr(settings, 'REDIS_URL') else 'redis://localhost:6379/' 140 | } 141 | } 142 | """) 143 | reporter.add_question_answer( 144 | question='cache support', 145 | answer=True 146 | ) 147 | check, message = api_generator.generate(diagram_path, app_name, True, git_support=git_support) 148 | else: 149 | reporter.add_question_answer( 150 | question='cache support', 151 | answer=False 152 | ) 153 | check, message = api_generator.generate(diagram_path, app_name, git_support=git_support) 154 | 155 | if check: 156 | stdout_messages.append(self.style.SUCCESS(f'{app_name}[INFO]: {message}')) 157 | else: 158 | stdout_messages.append(self.style.ERROR(f'{app_name}[ERROR]: {message}')) 159 | else: 160 | reporter.add_question_answer( 161 | question='create serializers.py', 162 | answer=False 163 | ) 164 | reporter.add_question_answer( 165 | question='create views.py', 166 | answer=False 167 | ) 168 | 169 | if create_test == 'yes': 170 | reporter.add_question_answer( 171 | question='create test_api.py', 172 | answer=True 173 | ) 174 | reporter.add_question_answer( 175 | question='create test_model.py', 176 | answer=True 177 | ) 178 | self.validate_settings(step='test') 179 | test_generator = TestGenerator() 180 | api_test = True if create_api == 'yes' else False 181 | model_test = True if create_model == 'yes' else False 182 | check, message = test_generator.generate( 183 | diagram_path, app_name, api_test=api_test, 184 | model_test=model_test, git_support=git_support) 185 | if check: 186 | stdout_messages.append(self.style.SUCCESS(f'{app_name}[INFO]: {message}')) 187 | else: 188 | stdout_messages.append(self.style.ERROR(f'{app_name}[ERROR]: {message}')) 189 | else: 190 | reporter.add_question_answer( 191 | question='create test_api.py', 192 | answer=False 193 | ) 194 | reporter.add_question_answer( 195 | question='create test_model.py', 196 | answer=False 197 | ) 198 | 199 | for message in stdout_messages: 200 | self.stdout.write(message) 201 | --------------------------------------------------------------------------------