├── .dockerignore ├── .gitattributes ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── deploy ├── nginx │ └── nginx.json-logging.conf └── uwsgi │ └── uwsgi.ini ├── docker-compose.yml ├── project_name ├── __init__.py ├── accounts │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── forms.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── createsuperuser.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── templatetags │ │ ├── __init__.py │ │ └── accounts.py │ ├── tests │ │ ├── __init__.py │ │ ├── factories.py │ │ ├── models_tests.py │ │ ├── utils_tests.py │ │ └── views_tests.py │ ├── urls.py │ ├── utils.py │ └── views.py ├── api │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ └── __init__.py │ ├── mixins.py │ ├── models.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── core │ ├── __init__.py │ ├── models.py │ └── tests │ │ └── __init__.py ├── lib │ ├── __init__.py │ ├── audit │ │ ├── LICENSE.txt │ │ ├── README.txt │ │ ├── __init__.py │ │ ├── middleware.py │ │ └── models.py │ ├── cache │ │ ├── __init__.py │ │ └── decorators.py │ ├── common │ │ ├── __init__.py │ │ ├── templatetags │ │ │ ├── __init__.py │ │ │ ├── dateutils.py │ │ │ ├── dicthelpers.py │ │ │ ├── formhelpers.py │ │ │ ├── list_helpers.py │ │ │ ├── numberhelpers.py │ │ │ ├── sitemaphelpers.py │ │ │ ├── statichelpers.py │ │ │ ├── stringhelpers.py │ │ │ ├── templatehelpers.py │ │ │ └── urlhelpers.py │ │ ├── utils.py │ │ └── views.py │ ├── datetime │ │ ├── __init__.py │ │ └── utils.py │ ├── forms │ │ ├── __init__.py │ │ └── generic.py │ ├── html │ │ ├── __init__.py │ │ ├── markdown_extensions.py │ │ └── utils.py │ ├── storage.py │ └── viewslibrary │ │ ├── __init__.py │ │ ├── tests.py │ │ └── views.py ├── manage.py ├── project_name │ ├── __init__.py │ ├── celery.py │ ├── databases.py │ ├── development │ │ ├── __init__.py │ │ └── settings.py │ ├── general │ │ ├── __init__.py │ │ ├── redis_rabbitmq.py │ │ └── settings.py │ ├── settings.py │ ├── staging │ │ ├── __init__.py │ │ └── settings.py │ ├── testing │ │ ├── __init__.py │ │ └── settings.py │ ├── third_parties │ │ ├── __init__.py │ │ └── blacklist_domains.py │ ├── urls.py │ └── wsgi.py ├── static │ ├── css │ │ ├── foundation │ │ │ ├── foundation.css │ │ │ └── foundation.min.css │ │ └── project_name.css │ └── js │ │ └── vendor │ │ ├── foundation │ │ ├── foundation.js │ │ └── foundation.min.js │ │ ├── jquery │ │ ├── jquery.js │ │ └── jquery.min.js │ │ └── jqueryui │ │ ├── jquery-ui-1.7.2.min.js │ │ └── jquery-ui.min.js └── templates │ └── project_name │ ├── application │ ├── core.html │ └── homepage │ │ └── homepage.html │ ├── auth │ ├── core.html │ └── login.html │ └── shared │ ├── forms │ ├── foundation_field.html │ └── foundation_field_basic.html │ └── partials │ └── messages.html ├── requirements ├── common.txt ├── deployment.txt ├── development.txt ├── live.txt └── windows │ ├── common.txt │ ├── deployment.txt │ └── development.txt └── roles └── tasks.py /.dockerignore: -------------------------------------------------------------------------------- 1 | *.pgdump 2 | .git 3 | *.swp 4 | *.swo 5 | *.pyc 6 | config.ini 7 | supervisord.log 8 | supervisord.pid 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.rb linguist-language=Python 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | LINUX-ENV 2 | ENV 3 | DEV 4 | EVENT 5 | popup 6 | syntax: glob 7 | *.pyc 8 | ._* 9 | __pycache__/ 10 | .#* 11 | #*# 12 | .DS_Store 13 | media 14 | local_settings.py 15 | *.mo 16 | *.codekit 17 | *.map 18 | .sass-cache/ 19 | .coverage 20 | wag-labels 21 | useful 22 | renders 23 | *~ 24 | .*.sw* 25 | .Python 26 | .vagrant/ 27 | renders 28 | .pip-download-cache 29 | *-auto-bak-*.pgdump 30 | .installed 31 | # built CSS is not kept in source control 32 | config.codekit 33 | *.map.css 34 | *.tar 35 | *.Rhistory 36 | *.tsv 37 | keys 38 | *.sql 39 | dump.rdb 40 | build 41 | 42 | # Eclipse metadata 43 | .project 44 | .pydevproject 45 | .settings 46 | dbbaks 47 | 48 | selenium-server-standalone*jar 49 | ingest 50 | 51 | # PyCharm metadata 52 | .idea 53 | 54 | # VS Code metadata 55 | .vscode 56 | .noseids 57 | 58 | celerybeat-schedule 59 | *.pgdump 60 | db.sqlite3 61 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ############################################################ 2 | # Dockerfile to run a Django-based web application 3 | # Based on an Ubuntu Image 4 | ############################################################ 5 | FROM ubuntu:18.04 6 | MAINTAINER tiago.arasilva@outlook.com 7 | 8 | ENV DEBIAN_FRONTEND noninteractive 9 | ENV LANG en_GB.UTF-8 10 | ENV LC_ALL en_GB.UTF-8 11 | 12 | # 13 | # Install Apt Packages 14 | # 15 | RUN apt-get update && \ 16 | apt-get upgrade -y && \ 17 | apt-get install curl -y && \ 18 | apt-get clean && apt-get autoclean && \ 19 | find /var/lib/apt/lists/ -type f -delete && \ 20 | echo "deb http://apt.postgresql.org/pub/repos/apt/ bionic-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \ 21 | curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - 22 | 23 | RUN apt-get update && \ 24 | apt-get install -y bind9-host \ 25 | build-essential \ 26 | curl \ 27 | geoip-bin \ 28 | gettext \ 29 | git-core \ 30 | gawk \ 31 | imagemagick \ 32 | iputils-ping \ 33 | language-pack-en \ 34 | less \ 35 | libcurl4-openssl-dev \ 36 | libevent-dev \ 37 | libffi-dev \ 38 | libgeos-c1v5 \ 39 | libmagickwand-dev \ 40 | libmemcached-tools \ 41 | libxml2-dev \ 42 | libxslt-dev \ 43 | memcached \ 44 | net-tools \ 45 | nginx-extras \ 46 | perl \ 47 | pgbouncer \ 48 | postgresql-client-9.6 \ 49 | postgresql-server-dev-9.6 \ 50 | python-pil \ 51 | python-urllib3 \ 52 | python3-pip \ 53 | python-pip \ 54 | python-dev \ 55 | python3-dev \ 56 | rsyslog \ 57 | socat \ 58 | software-properties-common \ 59 | sudo \ 60 | supervisor \ 61 | unattended-upgrades \ 62 | unzip \ 63 | vim \ 64 | wget && \ 65 | apt-get clean && apt-get autoclean && \ 66 | find /var/lib/apt/lists/ -type f -delete 67 | 68 | 69 | RUN apt-get -y upgrade 70 | 71 | # 72 | # Install Pip Requirements 73 | # upgrade the setuptools from 27.1.2 to 32.3.1. 74 | # 75 | RUN pip3 install pip --upgrade 76 | RUN pip3 install wheel 77 | RUN pip3 install -U setuptools 78 | 79 | # 80 | # Install requirements 81 | # 82 | ADD requirements /var/www/requirements 83 | RUN pip3 install -r /var/www/requirements/common.txt 84 | 85 | RUN echo 'alias python=python3' >> ~/.bashrc && echo 'alias pip=pip3' >> ~/.bashrc 86 | 87 | # Patch Nginx Config to Disable Security Tokens 88 | RUN sed -i -e 's/# server_tokens off;/server_tokens off;/g' /etc/nginx/nginx.conf 89 | 90 | # Add JSON logging formatter for nginx 91 | ADD deploy/nginx/nginx.json-logging.conf /etc/nginx/conf.d/json-logging.conf 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Tiago Silva 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | clean: clean_pyc 2 | clean_pyc: 3 | find . -type f -name "*.pyc" -delete || true 4 | 5 | migrate: 6 | python3 $(PROJECT_NAME)/manage.py migrate --noinput 7 | 8 | migrations: 9 | python3 $(PROJECT_NAME)/manage.py makemigrations 10 | 11 | reusedb_unittests: 12 | DJANGO_SETTINGS_MODULE=$(PROJECT_NAME).testing.settings REUSE_DB=1 DJANGOENV=testing python3 $(PROJECT_NAME)/manage.py test $(TESTONLY) --with-specplugin --keepdb 13 | 14 | unittests: 15 | DJANGO_SETTINGS_MODULE=$(PROJECT_NAME).testing.settings DJANGOENV=testing python3 $(PROJECT_NAME)/manage.py test $(TESTONLY) --with-specplugin 16 | 17 | run: 18 | python3 $(PROJECT_NAME)/manage.py runserver_plus 0.0.0.0:8000 --settings=$(PROJECT_NAME).$(ENVIRONMENT).settings 19 | 20 | shell: 21 | python3 $(PROJECT_NAME)/manage.py shell_plus --settings=$(PROJECT_NAME).$(ENVIRONMENT).settings 22 | 23 | celery_django: 24 | python3 $(PROJECT_NAME)/manage.py celery -A $(PROJECT_NAME) worker --logleve=INFO --settings=$(PROJECT_NAME).$(ENVIRONMENT).settings 25 | 26 | celery: 27 | python3 $(PROJECT_NAME)/manage.py celery worker -A $(PROJECT_NAME).celery --logleve=INFO --settings=$(PROJECT_NAME).$(ENVIRONMENT).settings 28 | 29 | show_urls: 30 | python3 $(PROJECT_NAME)/manage.py show_urls --settings=$(PROJECT_NAME).$(ENVIRONMENT).settings 31 | 32 | validate_templates: 33 | python3 $(PROJECT_NAME)/manage.py validate_templates --settings=$(PROJECT_NAME).$(ENVIRONMENT).settings 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License: MIT](https://img.shields.io/github/license/vintasoftware/django-react-boilerplate.svg)](LICENSE.txt) 2 | 3 | Django Docker Boilerplate - Django Rest Framework with Redis, RabbitMQ and Celery 4 | =============================================================================== 5 | 6 | # Notice 7 | - This boilerplate is no longer supported. Thank you very much for the contribution and it's now only supported the second version [Here](https://github.com/tiagoarasilva/django2-boilerplate) 8 | 9 | ## Note 10 | 11 | - The template is using foundation as a framework but this can be changed by your personal preference, you only need to update the references in the `static` and `templates` folder. 12 | - Inside the views located in accounts, there are some that you need to create your own files, such as the registration or change password, the code is there, you only need to place the HTML inside the proper directory. The only HTML placed and working is the login and the homepage to allow you to do a first run. 13 | - Comes with a bunch of plugins for many kinds of projects. Not all of them are needed, feel free to remove what you don't need 14 | - This boilerplate is now supporting [Django Channels](https://channels.readthedocs.io/en/stable/index.html). 15 | - Read more about this [here](https://channels.readthedocs.io/en/stable/index.html) 16 | 17 | # Installing from the template base 18 | 19 | - `django-admin startproject --template=https://github.com/tiagoarasilva/django-boilerplate/archive/master.zip --extension=py,md,html,txt,scss,sass project_name` 20 | - Make sure you change the "project_name" to the name you desire for you project. 21 | - The tests for the views won't work until you implement the solution to make the tests passing, that means, once you implement the views! 22 | 23 | # Docker 24 | 25 | - Change the {{ project_name }} in your docker file to the desired name gave to the project when running the previous command. 26 | 27 | # {{ project_name }} Docker 28 | 29 | - Run `docker volume create --name={{ project_name }}_db_data` 30 | - Run `docker-compose up`. It will download all the resources needed to build your docker containers 31 | - Run `docker-compose exec {{ project_name }} bash` to go inside the container 32 | - Run `make run` to start the server (inside docker container) 33 | - Run `make shell` to start the shell_plus 34 | 35 | If you desire, you can create somes aliases in your local machine, inside the bash_profile/bashrc/profile to do automatically some previous instructions for you 36 | 37 | E.g.: 38 | 39 | ```shell 40 | alias shell_plus='docker-compose exec {{ project_name }} bash && make run' 41 | alias run_server='docker-compose exec {{ project_name }} bash && make shell' 42 | ``` 43 | 44 | # First run with the project 45 | 46 | - Inside docker container: 47 | - Run `make migrate`. This is a special command inside the Makefile to run the first migration or if you are on windows or you don't want to run the Makefile, just run `python {{ project_name }}/manage.py migrate` 48 | - Run `python {{ project_name }}/manage.py createsuperuser` to create a super user for yourself 49 | - It will create a "User Admin" by default as first and last name respectively. This can be changed in `accounts/management/commands/createsuperuser.py` 50 | 51 | 52 | # Run Tests (If you ran migrations before and need to reconstruct the DB schema) 53 | 54 | `make unittests TESTONLY='profiles.tests.models_tests'` 55 | - OR 56 | `make unittests TESTONLY='profiles.tests.models_tests:ProfileUserTest.test_create_user'` for a specific test 57 | 58 | # If you only need to run the tests and the models weren't changed before 59 | 60 | `make reusedb_unittests TESTONLY='profiles.tests.models_tests` 61 | 62 | ### Apps 63 | 64 | All of your Django "apps" go in this {{ project_name}} directory. These have models, views, forms, urls, 65 | templates or all of the above. These should be Python packages you would add to 66 | your project's `INSTALLED_APPS` list. 67 | 68 | 69 | ### Requirements for MacOS and Windows 70 | 71 | Install Homebrew (MacOS Users) 72 | `/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"` 73 | 74 | Install OpenSSL or Upgrade (MacOS Users) 75 | `brew install openssl` 76 | 77 | ### Requirements for Linux (Ubuntu >= 16.10) 78 | Install OpenSSL or Upgrade 79 | `sudo apt update` 80 | `sudo apt install openssl-server` 81 | 82 | ### Requirements for Linux (Ubuntu <= 16.10) 83 | Install OpenSSL or Upgrade 84 | `sudo apt-get update` 85 | `sudo apt-get install openssl-server` 86 | 87 | 88 | Install VirtualenvWrapper 89 | `https://virtualenvwrapper.readthedocs.io/en/latest/install.html` 90 | 91 | Upgrade pip 92 | `pip install --upgrade pip` 93 | 94 | ### Templates 95 | 96 | Project-wide templates are located in templates/ 97 | 98 | ### Celery 99 | This projects integrates Redis with RabbitMQ and Celery 100 | -------------------------------------------------------------------------------- /deploy/nginx/nginx.json-logging.conf: -------------------------------------------------------------------------------- 1 | log_format json_format '{ "@timestamp": "$time_iso8601", ' 2 | '"@fields": { ' 3 | '"remote_addr": "$remote_addr", ' 4 | '"remote_user": "$remote_user", ' 5 | '"body_bytes_sent": "$body_bytes_sent", ' 6 | '"request_time": "$request_time", ' 7 | '"status": "$status", ' 8 | '"request": "$request", ' 9 | '"request_method": "$request_method", ' 10 | '"http_referrer": "$http_referer", ' 11 | '"http_user_agent": "$http_user_agent" } }'; 12 | -------------------------------------------------------------------------------- /deploy/uwsgi/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | enable-threads = true 3 | single-interpreter=true 4 | wsgi-file = # location of the uwsgi file 5 | touch-reload = # location of the uwsgi file 6 | touch-logreopen = # location of the uwsgi file 7 | chown-socket = www-data:www-data 8 | master = true 9 | workers=4 10 | ;processes = 10 11 | socket = /var/www/uwsgi.sock 12 | chmod-socket = 664 13 | vacuum = true 14 | harakiri = 300 15 | harakiri-verbose 16 | max-requests = 300 17 | log-zero 18 | log-slow 19 | log-500 20 | log-x-forwarded-for 21 | drop-after-apps 22 | buffer-size = 32768 23 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | db: 4 | restart: always 5 | image: postgres:11.6 6 | expose: 7 | - "5432" 8 | volumes: 9 | - "{{ project_name }}_db_data:/var/lib/postgresql/data" 10 | ports: 11 | - "5432:5432" 12 | 13 | redis: 14 | restart: always 15 | image: redis:latest 16 | expose: 17 | - "6379" 18 | 19 | rabbit: 20 | restart: always 21 | image: rabbitmq:3.6.12 22 | expose: 23 | - "5672" 24 | environment: 25 | - RABBITMQ_DEFAULT_USER=rabbit_user 26 | - RABBITMQ_DEFAULT_PASS=rabbit_user_default_pass 27 | 28 | {{ project_name }}: 29 | restart: always 30 | image: oratio/libra:latest 31 | depends_on: 32 | - redis 33 | - rabbit 34 | environment: 35 | DJANGOENV: development 36 | ENVIRONMENT: development 37 | PYTHON: python 38 | PROJECT_NAME: {{ project_name }} 39 | TERM: xterm 40 | ROLE: development 41 | links: 42 | - db:postgres 43 | - redis 44 | - rabbit 45 | ports: 46 | - "80:80" 47 | - "443:443" 48 | - "8000:8000" 49 | expose: 50 | - "80" 51 | - "443" 52 | - "8000" 53 | volumes: 54 | - .:/var/www 55 | - ~/.aws/config:/root/.aws/config 56 | working_dir: /var/www 57 | command: bash -lc "pip install invoke jinja2 && invoke -r roles $${ROLE}" 58 | 59 | volumes: 60 | {{ project_name }}_db_data: 61 | external: true 62 | -------------------------------------------------------------------------------- /project_name/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-boilerplate/35ed124d3290417037dcb382936eebe62dd2f13f/project_name/__init__.py -------------------------------------------------------------------------------- /project_name/accounts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-boilerplate/35ed124d3290417037dcb382936eebe62dd2f13f/project_name/accounts/__init__.py -------------------------------------------------------------------------------- /project_name/accounts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from accounts.models import User, Profile, ProfileType 4 | 5 | admin.site.register(User) 6 | admin.site.register(Profile) 7 | admin.site.register(ProfileType) 8 | -------------------------------------------------------------------------------- /project_name/accounts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountsConfig(AppConfig): 5 | name = 'accounts' 6 | 7 | -------------------------------------------------------------------------------- /project_name/accounts/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth import get_user_model 3 | from django.contrib.auth import authenticate 4 | from django.conf import settings 5 | from django.utils.translation import ugettext_lazy as _ 6 | from django.forms.models import modelformset_factory, inlineformset_factory 7 | from slugify import slugify 8 | 9 | import accounts.models 10 | import accounts.utils 11 | 12 | 13 | def check_email_domain_is_valid(email): 14 | if email.split("@")[-1].strip() in settings.BLACKLISTED_DOMAINS: 15 | raise forms.ValidationError(_("This email address is invalid.")) 16 | return True 17 | 18 | 19 | class LoginForm(forms.Form): 20 | 21 | email = forms.EmailField( 22 | label=_("Email"), 23 | widget=forms.EmailInput(attrs={'autofocus': 'autofocus', 'id': 'login_email', 'Placeholder': 'Email'})) 24 | password = forms.CharField( 25 | label=_("Password"), 26 | widget=forms.PasswordInput(attrs={'id': 'login_password', 'placeholder': 'Password'})) 27 | 28 | def clean(self): 29 | try: 30 | email = self.cleaned_data['email'] 31 | password = self.cleaned_data['password'] 32 | 33 | user = get_user_model().objects.get(email__iexact=email, is_active=True) 34 | try: 35 | self.authed_user = authenticate( 36 | username=user.username, 37 | password=password, 38 | ) 39 | except ValueError: 40 | self.authed_user = None 41 | 42 | if self.authed_user: 43 | return self.cleaned_data 44 | 45 | except (get_user_model().DoesNotExist, KeyError): 46 | pass 47 | 48 | raise forms.ValidationError("Your login details were incorrect. Please try again.") 49 | 50 | def get_user(self): 51 | return self.authed_user 52 | 53 | 54 | class RegistrationForm(forms.Form): 55 | 56 | first_name = forms.CharField(label=_('First name'), required=True, widget=forms.TextInput( 57 | attrs={ 58 | 'id': 'register_first_name', 59 | 'class': 'validate black-text' 60 | })) 61 | 62 | last_name = forms.CharField(label=_('Last name'), required=True, widget=forms.TextInput( 63 | attrs={ 64 | 'id': 'register_first_name', 65 | 'class': 'validate black-text' 66 | })) 67 | 68 | email = forms.EmailField(label=_("Email"), required=True, widget=forms.EmailInput( 69 | attrs={ 70 | 'class': 'validate black-text', 71 | 'id': 'register_email', 72 | })) 73 | 74 | password = forms.CharField(label=_("Password"), required=True, widget=forms.PasswordInput( 75 | attrs={ 76 | 'id': 'register_password', 77 | 'class': 'validate black-text', 78 | })) 79 | 80 | retype_password = forms.CharField(label=_("Repeat Password"), required=True, widget=forms.PasswordInput( 81 | attrs={ 82 | 'id': 'repeat_password', 83 | 'class': 'validate black-text', 84 | })) 85 | 86 | profile_type = forms.ChoiceField(choices=accounts.models.Choices.Profiles.PROFILE_CHOICES) 87 | 88 | def clean(self): 89 | cleaned_data = super(RegistrationForm, self).clean() 90 | self._validate_password() 91 | return cleaned_data 92 | 93 | def clean_first_name(self): 94 | first_name = self.cleaned_data['first_name'] 95 | 96 | if first_name.strip() == "" or first_name.strip() is None: 97 | raise forms.ValidationError(_("First name cannot be empty.")) 98 | return first_name 99 | 100 | def clean_last_name(self): 101 | first_name = self.cleaned_data['last_name'] 102 | 103 | if first_name.strip() == "" or first_name.strip() is None: 104 | raise forms.ValidationError(_("Last name cannot be empty.")) 105 | return first_name 106 | 107 | def clean_email(self, raise_on_duplicate=True): 108 | email = self.cleaned_data['email'].lower().strip().rstrip(".") 109 | try: 110 | self.user = get_user_model().objects.get(email__iexact=email) 111 | except get_user_model().DoesNotExist: 112 | pass 113 | except get_user_model().MultipleObjectsReturned: 114 | raise forms.ValidationError(_("There is already an account with that email address.")) 115 | else: 116 | if raise_on_duplicate or self.user.has_usable_password(): 117 | raise forms.ValidationError(_("There is already an account with that email address.")) 118 | 119 | check_email_domain_is_valid(email) 120 | return email 121 | 122 | def clean_password(self): 123 | password = self.cleaned_data['password'].strip() 124 | 125 | if not password: 126 | self.add_error('password', _("The password cannot be empty")) 127 | return password 128 | 129 | def clean_retype_password(self): 130 | retype_password = self.cleaned_data['retype_password'].strip() 131 | 132 | if not retype_password: 133 | self.add_error('retype_password', _("The password cannot be empty")) 134 | 135 | self._validate_password() 136 | return retype_password 137 | 138 | def _validate_password(self): 139 | password = self.cleaned_data.get('password') 140 | retype_password = self.cleaned_data.get('retype_password') 141 | 142 | if password != retype_password: 143 | self.add_error('password', _("The passwords do not match")) 144 | 145 | -------------------------------------------------------------------------------- /project_name/accounts/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-boilerplate/35ed124d3290417037dcb382936eebe62dd2f13f/project_name/accounts/management/__init__.py -------------------------------------------------------------------------------- /project_name/accounts/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-boilerplate/35ed124d3290417037dcb382936eebe62dd2f13f/project_name/accounts/management/commands/__init__.py -------------------------------------------------------------------------------- /project_name/accounts/management/commands/createsuperuser.py: -------------------------------------------------------------------------------- 1 | """ 2 | Management utility to create superusers. 3 | """ 4 | import getpass 5 | import sys 6 | 7 | from django.contrib.auth import get_user_model 8 | from django.contrib.auth.management import get_default_username 9 | from django.contrib.auth.password_validation import validate_password 10 | from django.core import exceptions 11 | from django.core.management.base import BaseCommand, CommandError 12 | from django.db import DEFAULT_DB_ALIAS 13 | from django.utils.text import capfirst 14 | 15 | from accounts.models import Profile, ProfileType 16 | from accounts.utils import generate_username, generate_profile_type 17 | 18 | 19 | class NotRunningInTTYException(Exception): 20 | pass 21 | 22 | 23 | class Command(BaseCommand): 24 | help = 'Used to create a superuser.' 25 | requires_migrations_checks = True 26 | stealth_options = ('stdin',) 27 | 28 | def __init__(self, *args, **kwargs): 29 | super().__init__(*args, **kwargs) 30 | self.UserModel = get_user_model() 31 | self.username_field = self.UserModel._meta.get_field(self.UserModel.USERNAME_FIELD) 32 | 33 | def add_arguments(self, parser): 34 | parser.add_argument( 35 | '--%s' % self.UserModel.USERNAME_FIELD, 36 | dest=self.UserModel.USERNAME_FIELD, default=None, 37 | help='Specifies the login for the superuser.', 38 | ) 39 | parser.add_argument( 40 | '--noinput', '--no-input', action='store_false', dest='interactive', 41 | help=( 42 | 'Tells Django to NOT prompt the user for input of any kind. ' 43 | 'You must use --%s with --noinput, along with an option for ' 44 | 'any other required field. Superusers created with --noinput will ' 45 | 'not be able to log in until they\'re given a valid password.' % 46 | self.UserModel.USERNAME_FIELD 47 | ), 48 | ) 49 | parser.add_argument( 50 | '--database', action='store', dest='database', 51 | default=DEFAULT_DB_ALIAS, 52 | help='Specifies the database to use. Default is "default".', 53 | ) 54 | for field in self.UserModel.REQUIRED_FIELDS: 55 | parser.add_argument( 56 | '--%s' % field, dest=field, default=None, 57 | help='Specifies the %s for the superuser.' % field, 58 | ) 59 | 60 | def execute(self, *args, **options): 61 | self.stdin = options.get('stdin', sys.stdin) # Used for testing 62 | return super().execute(*args, **options) 63 | 64 | def handle(self, *args, **options): 65 | username = options[self.UserModel.USERNAME_FIELD] 66 | database = options['database'] 67 | 68 | # If not provided, create the user with an unusable password 69 | password = None 70 | user_data = {} 71 | # Same as user_data but with foreign keys as fake model instances 72 | # instead of raw IDs. 73 | fake_user_data = {} 74 | 75 | # Do quick and dirty validation if --noinput 76 | if not options['interactive']: 77 | try: 78 | if not username: 79 | raise CommandError("You must use --%s with --noinput." % self.UserModel.USERNAME_FIELD) 80 | username = self.username_field.clean(username, None) 81 | 82 | for field_name in self.UserModel.REQUIRED_FIELDS: 83 | if options[field_name]: 84 | field = self.UserModel._meta.get_field(field_name) 85 | user_data[field_name] = field.clean(options[field_name], None) 86 | else: 87 | raise CommandError("You must use --%s with --noinput." % field_name) 88 | except exceptions.ValidationError as e: 89 | raise CommandError('; '.join(e.messages)) 90 | 91 | else: 92 | # Prompt for username/password, and any other required fields. 93 | # Enclose this whole thing in a try/except to catch 94 | # KeyboardInterrupt and exit gracefully. 95 | default_username = get_default_username() 96 | try: 97 | 98 | if hasattr(self.stdin, 'isatty') and not self.stdin.isatty(): 99 | raise NotRunningInTTYException("Not running in a TTY") 100 | 101 | # Get a username 102 | verbose_field_name = self.username_field.verbose_name 103 | while username is None: 104 | input_msg = capfirst(verbose_field_name) 105 | if default_username: 106 | input_msg += " (leave blank to use '%s')" % default_username 107 | username_rel = self.username_field.remote_field 108 | input_msg = '%s%s: ' % ( 109 | input_msg, 110 | ' (%s.%s)' % ( 111 | username_rel.model._meta.object_name, 112 | username_rel.field_name 113 | ) if username_rel else '' 114 | ) 115 | username = self.get_input_data(self.username_field, input_msg, default_username) 116 | if not username: 117 | continue 118 | if self.username_field.unique: 119 | try: 120 | self.UserModel._default_manager.db_manager(database).get_by_natural_key(username) 121 | except self.UserModel.DoesNotExist: 122 | pass 123 | else: 124 | self.stderr.write("Error: That %s is already taken." % verbose_field_name) 125 | username = None 126 | 127 | for field_name in self.UserModel.REQUIRED_FIELDS: 128 | field = self.UserModel._meta.get_field(field_name) 129 | user_data[field_name] = options[field_name] 130 | while user_data[field_name] is None: 131 | message = '%s%s: ' % ( 132 | capfirst(field.verbose_name), 133 | ' (%s.%s)' % ( 134 | field.remote_field.model._meta.object_name, 135 | field.remote_field.field_name, 136 | ) if field.remote_field else '', 137 | ) 138 | input_value = self.get_input_data(field, message) 139 | user_data[field_name] = input_value 140 | fake_user_data[field_name] = input_value 141 | 142 | # Wrap any foreign keys in fake model instances 143 | if field.remote_field: 144 | fake_user_data[field_name] = field.remote_field.model(input_value) 145 | 146 | # Get a password 147 | while password is None: 148 | password = getpass.getpass() 149 | password2 = getpass.getpass('Password (again): ') 150 | if password != password2: 151 | self.stderr.write("Error: Your passwords didn't match.") 152 | password = None 153 | # Don't validate passwords that don't match. 154 | continue 155 | 156 | if password.strip() == '': 157 | self.stderr.write("Error: Blank passwords aren't allowed.") 158 | password = None 159 | # Don't validate blank passwords. 160 | continue 161 | 162 | try: 163 | validate_password(password2, self.UserModel(**fake_user_data)) 164 | except exceptions.ValidationError as err: 165 | self.stderr.write('\n'.join(err.messages)) 166 | password = None 167 | 168 | except KeyboardInterrupt: 169 | self.stderr.write("\nOperation cancelled.") 170 | sys.exit(1) 171 | 172 | except NotRunningInTTYException: 173 | self.stdout.write( 174 | "Superuser creation skipped due to not running in a TTY. " 175 | "You can run `manage.py createsuperuser` in your project " 176 | "to create one manually." 177 | ) 178 | 179 | if username: 180 | user_data[self.UserModel.USERNAME_FIELD] = username 181 | user_data['password'] = password 182 | self.UserModel._default_manager.db_manager(database).create_superuser(**user_data) 183 | if options['verbosity'] >= 1: 184 | self.stdout.write("Superuser created successfully.") 185 | 186 | self.generate_profile_from_user(**user_data) 187 | 188 | def get_input_data(self, field, message, default=None): 189 | """ 190 | Override this method if you want to customize data inputs or 191 | validation exceptions. 192 | """ 193 | raw_value = input(message) 194 | if default and raw_value == '': 195 | raw_value = default 196 | try: 197 | val = field.clean(raw_value, None) 198 | except exceptions.ValidationError as e: 199 | self.stderr.write("Error: %s" % '; '.join(e.messages)) 200 | val = None 201 | 202 | return val 203 | 204 | def generate_profile_from_user(self, **user_data): 205 | from slugify import slugify 206 | user = get_user_model().objects.get(email=user_data.get('email')) 207 | user.first_name = u'User' 208 | user.last_name = u'Admin' 209 | 210 | username = generate_username(user.username) 211 | profile = Profile.objects.create( 212 | user=user, slug=slugify(username), 213 | ) 214 | generate_profile_type(profile) 215 | user.save() 216 | profile.save() 217 | -------------------------------------------------------------------------------- /project_name/accounts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.4 on 2017-09-04 17:41 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | import django.contrib.auth.models 7 | import django.contrib.auth.validators 8 | from django.db import migrations, models 9 | import django.db.models.deletion 10 | import django.utils.timezone 11 | 12 | 13 | class Migration(migrations.Migration): 14 | 15 | initial = True 16 | 17 | dependencies = [ 18 | ('auth', '0008_alter_user_username_max_length'), 19 | ] 20 | 21 | operations = [ 22 | migrations.CreateModel( 23 | name='User', 24 | fields=[ 25 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 26 | ('password', models.CharField(max_length=128, verbose_name='password')), 27 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 28 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 29 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), 30 | ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), 31 | ('last_name', models.CharField(blank=True, max_length=30, verbose_name='last name')), 32 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), 33 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 34 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 35 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 36 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), 37 | ], 38 | options={ 39 | 'db_table': 'auth_user', 40 | 'permissions': (('can_view_dashboard', 'Can view all dashboards'), ('can_view_store_profiles', 'Can store profiles')), 41 | }, 42 | managers=[ 43 | ('objects', django.contrib.auth.models.UserManager()), 44 | ], 45 | ), 46 | migrations.CreateModel( 47 | name='Profile', 48 | fields=[ 49 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 50 | ('slug', models.SlugField(help_text='Slug', max_length=255, unique=True)), 51 | ('created_at', models.DateTimeField(auto_now_add=True)), 52 | ('modified_at', models.DateTimeField(auto_now=True)), 53 | ('is_hidden', models.BooleanField(default=False)), 54 | ('is_disabled', models.BooleanField(default=False)), 55 | ('is_password_changed', models.BooleanField(default=False)), 56 | ('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), 57 | ], 58 | ), 59 | migrations.CreateModel( 60 | name='ProfileType', 61 | fields=[ 62 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 63 | ('profile_type', models.CharField(choices=[('admin', 'Admin'), ('user', 'User'), ('manager', 'Manager'), ('other', 'Other')], default='admin', max_length=255)), 64 | ('profile', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='profile_types', to='accounts.Profile')), 65 | ], 66 | ), 67 | migrations.CreateModel( 68 | name='Permission', 69 | fields=[ 70 | ], 71 | options={ 72 | 'permissions': (('shorten_urls', 'Can shorten urls in the dashboard'),), 73 | 'proxy': True, 74 | 'indexes': [], 75 | }, 76 | bases=('auth.permission',), 77 | managers=[ 78 | ('objects', django.contrib.auth.models.PermissionManager()), 79 | ], 80 | ), 81 | migrations.AddField( 82 | model_name='user', 83 | name='user_permissions', 84 | field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'), 85 | ), 86 | ] 87 | -------------------------------------------------------------------------------- /project_name/accounts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-boilerplate/35ed124d3290417037dcb382936eebe62dd2f13f/project_name/accounts/migrations/__init__.py -------------------------------------------------------------------------------- /project_name/accounts/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import bleach 3 | 4 | from django.conf import settings 5 | from django.utils.translation import ugettext_lazy as _ 6 | from django.core.urlresolvers import reverse 7 | from django.contrib.auth.models import Permission as DjangoPermission 8 | from django.contrib.auth.models import AbstractUser 9 | from django.db.models.signals import post_init, post_save 10 | from django.db import models 11 | 12 | from lib.cache.decorators import memoize_invalidate 13 | 14 | 15 | class Choices(object): 16 | 17 | class Profiles: 18 | ADMIN = u'admin' 19 | USER = u'user' 20 | MANAGER = u'manager' 21 | OTHER = u'other' 22 | 23 | PROFILE_CHOICES = ( 24 | (ADMIN, 'Admin'), 25 | (USER, 'User'), 26 | (MANAGER, 'Manager'), 27 | (OTHER, 'Other'), 28 | ) 29 | 30 | 31 | class ProfileType(models.Model): 32 | profile = models.ForeignKey('accounts.Profile', null=False, blank=True, related_name='profile_types') 33 | profile_type = models.CharField(max_length=255, choices=Choices.Profiles.PROFILE_CHOICES, 34 | default=Choices.Profiles.USER, null=False, blank=False) 35 | 36 | def __str__(self): 37 | return self.get_profile_type_display() 38 | 39 | 40 | class Profile(models.Model): 41 | """ 42 | For every user, there is a profile associated. This model is a representation of that same profile 43 | """ 44 | 45 | user = models.OneToOneField(settings.AUTH_USER_MODEL, null=True, blank=True, related_name='profile') 46 | slug = models.SlugField(max_length=255, help_text=_('Slug'), blank=False, null=False, unique=True) 47 | created_at = models.DateTimeField(null=False, blank=False, auto_now_add=True) 48 | modified_at = models.DateTimeField(null=False, blank=False, auto_now=True) 49 | is_hidden = models.BooleanField(default=False, blank=False, null=False) 50 | is_disabled = models.BooleanField(default=False, blank=False, null=False) 51 | is_password_changed = models.BooleanField(default=False, blank=False, null=False) 52 | 53 | def __str__(self): 54 | return self.slug 55 | 56 | 57 | class User(AbstractUser): 58 | """ 59 | Model responsible for the user maintenance for the platform 60 | """ 61 | class Meta: 62 | db_table = 'auth_user' 63 | permissions = (('can_view_dashboard', 'Can view all dashboards'), 64 | ('can_view_store_profiles', 'Can store profiles'),) 65 | 66 | @memoize_invalidate 67 | def get_or_create_profile(self, first_name=None, last_name=None): 68 | try: 69 | return Profile.objects.get(user=self) 70 | except Profile.DoesNotExist: 71 | profile = Profile.objects.create( 72 | user=self, slug=self.username 73 | ) 74 | profile.save() 75 | return profile 76 | 77 | def to_json_dict(self, context=None): 78 | data = { 79 | 'id': self.pk, 80 | 'username': bleach.clean(self.username), 81 | 'email_url': reverse('postman_write', args=(self.pk,)) 82 | } 83 | 84 | profile = self.profile 85 | if profile is not None: 86 | data['profile'] = profile.to_json_dict(context) 87 | 88 | return data 89 | 90 | def get_primary_email(self): 91 | return self.email 92 | 93 | @staticmethod 94 | def post_init(sender, **kwargs): 95 | instance = kwargs["instance"] 96 | instance.first_name = bleach.clean(instance.first_name) 97 | instance.last_name = bleach.clean(instance.last_name) 98 | instance.old_email = instance.email 99 | instance.old_is_active = instance.is_active 100 | instance.old_first_name = instance.first_name 101 | instance.old_last_name = instance.last_name 102 | 103 | @staticmethod 104 | def post_save(sender, **kwargs): 105 | instance = kwargs["instance"] 106 | try: 107 | profile = instance.profile 108 | except Profile.DoesNotExist: 109 | pass 110 | instance.old_email = instance.email 111 | instance.old_first_name = instance.first_name 112 | instance.old_last_name = instance.last_name 113 | 114 | @property 115 | def display_name(self): 116 | return self.first_name + u' ' + self.last_name 117 | 118 | 119 | post_init.connect(User.post_init, sender=User) 120 | post_save.connect(User.post_save, sender=User) 121 | 122 | 123 | class Permission(DjangoPermission): 124 | 125 | class Meta: 126 | proxy = True 127 | permissions = ( 128 | ('shorten_urls', 'Can shorten urls anywhere'), 129 | ) 130 | -------------------------------------------------------------------------------- /project_name/accounts/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-boilerplate/35ed124d3290417037dcb382936eebe62dd2f13f/project_name/accounts/templatetags/__init__.py -------------------------------------------------------------------------------- /project_name/accounts/templatetags/accounts.py: -------------------------------------------------------------------------------- 1 | from django.template import Library 2 | from accounts.models import ProfileType 3 | 4 | register = Library() 5 | 6 | 7 | @register.simple_tag 8 | def profiles(user): 9 | profile_type = ProfileType.objects.filter( profile=user.profile) 10 | return profile_type 11 | -------------------------------------------------------------------------------- /project_name/accounts/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-boilerplate/35ed124d3290417037dcb382936eebe62dd2f13f/project_name/accounts/tests/__init__.py -------------------------------------------------------------------------------- /project_name/accounts/tests/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | import uuid 3 | import factory.django 4 | 5 | from django.contrib.auth import get_user_model 6 | import accounts.models 7 | 8 | 9 | class ProfileFactory(factory.django.DjangoModelFactory): 10 | slug = factory.LazyAttribute(lambda u: "testes-%s" % str(uuid.uuid4())[0:12]) 11 | 12 | class Meta: 13 | model = accounts.models.Profile 14 | 15 | 16 | class ProfileTypeFactory(factory.django.DjangoModelFactory): 17 | profile = factory.SubFactory(ProfileFactory) 18 | profile_type = accounts.models.Choices.Profiles.USER 19 | 20 | class Meta: 21 | model = accounts.models.ProfileType 22 | 23 | 24 | class UserFactory(factory.django.DjangoModelFactory): 25 | username = factory.Sequence(lambda n: "testes-%s" % n) 26 | password = "testes" 27 | first_name = "Test" 28 | last_name = 'User' 29 | email = factory.LazyAttribute(lambda u: "%s@testes.example.com" % u.username) 30 | profile = factory.RelatedFactory(ProfileFactory, factory_related_name='user') 31 | 32 | class Meta: 33 | model = get_user_model() 34 | 35 | @classmethod 36 | def _prepare(cls, create, **kwargs): 37 | user = super()._prepare(create, **kwargs) 38 | password = kwargs.pop('password', None) 39 | if password: 40 | user.set_password(password) 41 | if create: 42 | user.save() 43 | return user 44 | -------------------------------------------------------------------------------- /project_name/accounts/tests/models_tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | import accounts.tests.factories 4 | import accounts.models 5 | 6 | 7 | class UserAccountModelTest(TestCase): 8 | 9 | def setUp(self): 10 | self.user = accounts.tests.factories.UserFactory(username='stuff') 11 | self.user_2 = accounts.tests.factories.UserFactory(username='{{ project_name }}_2') 12 | 13 | def test_create_user(self): 14 | self.assertEqual('stuff', self.user.username) 15 | 16 | def test_create_user_with_specific_email(self): 17 | self.user.email = '{{ project_name }}@test.com' 18 | self.user.save() 19 | 20 | self.assertEqual(self.user.email, '{{ project_name }}@test.com') 21 | 22 | def test_create_user_with_specific_password(self): 23 | self.user.password = '123password' 24 | self.user.save() 25 | 26 | self.assertEqual(self.user.password, '123password') 27 | 28 | def test_create_user_with_staff_access(self): 29 | self.user.is_staff = True 30 | self.user.save() 31 | 32 | self.assertEqual(self.user.is_staff, True) 33 | 34 | def test_can_create_more_than_one_user_at_the_same_time(self): 35 | self.assertNotEqual(self.user, self.user_2) 36 | 37 | 38 | class ProfileTypeTests(TestCase): 39 | 40 | def setUp(self): 41 | self.profile = accounts.tests.factories.ProfileFactory() 42 | self.profile_type = accounts.tests.factories.ProfileTypeFactory() 43 | 44 | def test_can_create_profile_type_object(self): 45 | self.profile_type.profile_type = accounts.models.Choices.Profiles.ADMIN 46 | self.profile_type.save() 47 | 48 | self.assertEqual(self.profile_type.profile_type, accounts.models.Choices.Profiles.ADMIN) 49 | 50 | def test_can_have_multiple_profile_types(self): 51 | 52 | accounts.tests.factories.ProfileTypeFactory( 53 | profile=self.profile, 54 | profile_type=accounts.models.Choices.Profiles.USER 55 | ) 56 | 57 | accounts.tests.factories.ProfileTypeFactory( 58 | profile=self.profile, 59 | profile_type=accounts.models.Choices.Profiles.ADMIN 60 | ) 61 | 62 | profiles = accounts.models.ProfileType.objects.filter( 63 | profile=self.profile 64 | ) 65 | 66 | self.assertEqual(profiles.count(), 2) 67 | -------------------------------------------------------------------------------- /project_name/accounts/tests/utils_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.test import TestCase 3 | from slugify import slugify 4 | 5 | import accounts.tests.factories 6 | import accounts.utils 7 | 8 | 9 | class UtilsTest(TestCase): 10 | 11 | def setUp(self): 12 | self.user = accounts.tests.factories.UserFactory(username=u'{{ project_name }}-pórto') 13 | 14 | def test_can_slugify_string(self): 15 | result = slugify(self.user.username) 16 | 17 | self.assertEqual(result, u'{{ project_name }}-porto') 18 | 19 | def test_can_update_profile_slug(self): 20 | profile = accounts.tests.factories.ProfileFactory(slug=u'slug!déstá-coisa') 21 | user = accounts.tests.factories.UserFactory(profile=profile, username=u'slug!déstá-coisa') 22 | slug_slugified = slugify(user.username) 23 | 24 | self.assertEqual(slug_slugified, u'slug-desta-coisa') 25 | 26 | profile_expected = accounts.utils.update_profile_slug(profile, user) 27 | user.username = profile.slug 28 | user.save() 29 | 30 | self.assertEqual(profile_expected.slug, u'slug-desta-coisa') 31 | self.assertEqual(profile_expected.slug, user.username) 32 | self.assertEqual(user.username, u'slug-desta-coisa') 33 | -------------------------------------------------------------------------------- /project_name/accounts/tests/views_tests.py: -------------------------------------------------------------------------------- 1 | from django_webtest import WebTest 2 | from django.core.urlresolvers import reverse 3 | 4 | import accounts.tests.factories 5 | import accounts.models 6 | import {{ project_name }}.third_parties.blacklist_domains 7 | 8 | 9 | class ListUsersViewTest(WebTest): 10 | 11 | def setUp(self): 12 | self.user = accounts.tests.factories.UserFactory(username=u'{{ project_name }}', email="cenas@cenas.com") 13 | self.url = reverse('profiles:list-users') 14 | 15 | def test_get_login_page(self): 16 | response = self.app.get(self.url, user=self.user) 17 | 18 | self.assertEqual(200, response.status_code) 19 | 20 | 21 | class LoginAndRegisterViewTest(WebTest): 22 | 23 | def setUp(self): 24 | self.user = accounts.tests.factories.UserFactory(username=u'{{ project_name }}', email="cenas@cenas.com") 25 | self.url_login = reverse('login') 26 | 27 | def _get_profile_page_data(self): 28 | return reverse('profiles:view-profile', kwargs={ 29 | 'slug': self.user.profile.slug 30 | }) 31 | 32 | def test_get_login_page(self): 33 | response = self.app.get(self.url_login) 34 | 35 | self.assertEqual(200, response.status_code) 36 | 37 | def test_can_get_the_profile_page_for_logged_user(self): 38 | response = self.app.get(self._get_profile_page_data(), user=self.user) 39 | 40 | self.assertEqual(200, response.status_code) 41 | 42 | 43 | class AddUserViewTest(WebTest): 44 | def setUp(self): 45 | self.user = accounts.tests.factories.UserFactory(username=u'{{ project_name }}') 46 | self.url = reverse('profiles:add-user') 47 | 48 | def _get_user_form_data(self, email='testeFirst@testeLast.com'): 49 | page = self.app.get(self.url, user=self.user) 50 | form = page.forms[0] 51 | form['first_name'] = 'testeFirst' 52 | form['last_name'] = 'testeLast' 53 | form['email'] = email 54 | form['password'] = '12345678' 55 | form['retype_password'] = '12345678' 56 | form['profile_type'] = accounts.models.Choices.ProfileType.STORE_PROFILES 57 | response = form.submit() 58 | 59 | return page, response 60 | 61 | def test_can_get_to_the_add_view_page(self): 62 | page = self.app.get(self.url, user=self.user) 63 | 64 | self.assertEqual(200, page.status_code) 65 | 66 | def test_can_add_new_user(self): 67 | page, response = self._get_user_form_data() 68 | 69 | self.assertEqual(302, response.status_code) 70 | 71 | def test_user_added_has_correct_profile_type(self): 72 | page, response = self._get_user_form_data() 73 | profile_wanted = accounts.models.Profile.objects.get(slug=self.user.profile.slug) 74 | profile_wanted.profile_type = accounts.models.Choices.ProfileType.STORE_PROFILES 75 | profile_wanted.save() 76 | 77 | self.assertEqual(profile_wanted.slug, self.user.profile.slug) 78 | self.assertEqual(profile_wanted.profile_type, accounts.models.Choices.ProfileType.STORE_PROFILES) 79 | 80 | def test_add_users_with_same_email_fails(self): 81 | page, response = self._get_user_form_data() 82 | page_now, response_now = self._get_user_form_data() 83 | 84 | self.assertIn(u'There is already an account with that email address.', str(response_now.content)) 85 | 86 | def test_add_user_without_fields_that_are_required(self): 87 | page = self.app.get(self.url, user=self.user) 88 | form = page.forms[0] 89 | form['first_name'] = '123' 90 | form['last_name'] = '' 91 | form['email'] = '123@456.com' 92 | form['password'] = '12345678' 93 | form['retype_password'] = '' 94 | form['profile_type'] = accounts.models.Choices.ProfileType.STORE_PROFILES 95 | response = form.submit() 96 | 97 | self.assertFormError(response, 'form', 'last_name', 'This field is required.') 98 | self.assertFormError(response, 'form', 'retype_password', 'This field is required.') 99 | 100 | def test_add_user_with_blacklisted_domain(self): 101 | b_list = {{ project_name }}.third_parties.blacklist_domains.BLACKLISTED_DOMAINS 102 | import random 103 | email = 'test@' + random.sample(b_list, 1)[0] 104 | page, response = self._get_user_form_data(email=email) 105 | 106 | self.assertIn(u'This email address is invalid.', str(response.content)) 107 | 108 | -------------------------------------------------------------------------------- /project_name/accounts/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | import accounts.views 4 | 5 | accounts_urlpatterns = [ 6 | url(r'^user/create/$', accounts.views.RegisterProfileView.as_view(), name='add-user'), 7 | ] 8 | -------------------------------------------------------------------------------- /project_name/accounts/utils.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from slugify import slugify 4 | 5 | import accounts.models 6 | 7 | 8 | def generate_username(first_name): 9 | uuid_user = str(uuid.uuid4()) 10 | username = first_name.lower() + "-{id}".format(id=uuid_user[:12]) 11 | return username 12 | 13 | 14 | def generate_profile_type(profile): 15 | profile_type = accounts.models.ProfileType.objects.create( 16 | profile=profile, profile_type=accounts.models.Choices.Profiles.ADMIN 17 | ) 18 | return profile_type 19 | 20 | 21 | def update_profile_slug(profile, user): 22 | try: 23 | slug = slugify(user.username) 24 | profile.slug = slug 25 | profile.save() 26 | return profile 27 | except accounts.models.Profile.DoesNotExist: 28 | pass 29 | -------------------------------------------------------------------------------- /project_name/accounts/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | from django.contrib.auth import get_user_model 3 | from django.contrib.auth import login, logout, update_session_auth_hash 4 | from django.core.urlresolvers import reverse 5 | from django.db import IntegrityError 6 | from django.db.models import Q 7 | from django.http import HttpResponseRedirect 8 | from django.utils.translation import ugettext_lazy as _ 9 | from django.views.generic import View, TemplateView, FormView, UpdateView, ListView, CreateView 10 | from lib.common.views import BaseTemplateMixin 11 | 12 | import accounts.forms 13 | import accounts.models 14 | import accounts.utils 15 | import accounts.models 16 | 17 | 18 | def login_and_handle_data_stored_in_session(user, request): 19 | session_key = request.session.session_key 20 | login(request, user) 21 | 22 | 23 | class BaseUserSystemView(BaseTemplateMixin): 24 | 25 | def get_object(self): 26 | user = accounts.models.User.objects.get( 27 | profile__slug=self.kwargs.get('slug', self.request.user.profile.slug) 28 | ) 29 | if self.request.user.email == user.email: 30 | return self.request.user 31 | return user 32 | 33 | def get_back_url(self): 34 | return self.request.META.get("HTTP_REFERER", reverse('homepage')) 35 | 36 | def get_context_data(self, **kwargs): 37 | context = super().get_context_data(**kwargs) 38 | context.update({ 39 | 'user': self.get_object(), 40 | 'active': False 41 | }) 42 | return context 43 | 44 | 45 | class LoginView(FormView): 46 | template_name = '{{ project_name }}/auth/login.html' 47 | form_class = accounts.forms.LoginForm 48 | 49 | def get(self, request, *args, **kwargs): 50 | if request.user.is_authenticated(): 51 | return HttpResponseRedirect(self.get_success_url()) 52 | return super().get(request, *args, **kwargs) 53 | 54 | def form_valid(self, form): 55 | user = form.get_user() 56 | login_and_handle_data_stored_in_session(user, self.request) 57 | return HttpResponseRedirect(self.get_success_url()) 58 | 59 | def form_invalid(self, form): 60 | messages.add_message( 61 | self.request, messages.ERROR, _("The credentials that you've entered don't match any account") 62 | ) 63 | return super().form_invalid(form) 64 | 65 | def get_success_url(self): 66 | return reverse('homepage') 67 | 68 | 69 | class RegisterProfileView(FormView): 70 | template_name = '{{ project_name }}/application/configurations/add-user.html' 71 | form_class = accounts.forms.RegistrationForm 72 | 73 | def form_valid(self, form): 74 | first_name = form.data['first_name'] 75 | last_name = form.data['last_name'] 76 | email = form.data['email'] 77 | password = form.data['password'] 78 | profile_type = form.data['profile_type'] 79 | username = accounts.utils.generate_username(first_name) 80 | 81 | try: 82 | user = get_user_model().objects.create_user( 83 | username=username, email=email, password=password, 84 | is_staff=False, is_superuser=False, first_name=first_name, last_name=last_name 85 | ) 86 | 87 | except IntegrityError: 88 | msg = _("There was a problem processing your information, please try again") 89 | messages.add_message(self.request, messages.ERROR, _(msg)) 90 | return super().form_valid(form) 91 | 92 | user.save() 93 | profile = user.get_or_create_profile(first_name=first_name, last_name=last_name) 94 | profile.profile_type = profile_type 95 | profile.save() 96 | 97 | msg = _("The profile for {email} has been created").format( 98 | email=user.email 99 | ) 100 | messages.add_message(self.request, messages.SUCCESS, _(msg)) 101 | return HttpResponseRedirect(self.get_success_url()) 102 | 103 | def form_invalid(self, form): 104 | for errors in form.errors.items(): 105 | key, msg = errors 106 | messages.add_message(self.request, messages.ERROR, _(msg[0])) 107 | return super().form_invalid(form) 108 | 109 | def get_success_url(self): 110 | return reverse('homepage') 111 | 112 | 113 | class LogoutView(View): 114 | def get(self, request, *args, **kwargs): 115 | logout(self.request) 116 | return HttpResponseRedirect(reverse('login')) 117 | 118 | 119 | class HomepageView(TemplateView): 120 | template_name = '{{ project_name }}/application/homepage/homepage.html' 121 | 122 | def get(self, request, *args, **kwargs): 123 | if request.user.is_authenticated(): 124 | return super(HomepageView, self).get(request, *args, **kwargs) 125 | return HttpResponseRedirect(reverse('login')) 126 | 127 | -------------------------------------------------------------------------------- /project_name/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-boilerplate/35ed124d3290417037dcb382936eebe62dd2f13f/project_name/api/__init__.py -------------------------------------------------------------------------------- /project_name/api/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /project_name/api/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ApiConfig(AppConfig): 5 | name = 'api' 6 | -------------------------------------------------------------------------------- /project_name/api/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-boilerplate/35ed124d3290417037dcb382936eebe62dd2f13f/project_name/api/migrations/__init__.py -------------------------------------------------------------------------------- /project_name/api/mixins.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | from rest_framework import authentication 3 | from rest_framework.authentication import SessionAuthentication, BasicAuthentication 4 | from rest_framework.permissions import IsAuthenticated 5 | from rest_framework import exceptions 6 | 7 | 8 | class AnonymousAuthentication(authentication.BaseAuthentication): 9 | 10 | def authenticate(self, request): 11 | return (request._request.user, None) 12 | 13 | 14 | class BaseAuthView(permissions.BasePermission): 15 | authentication_classes = (SessionAuthentication, BasicAuthentication,) 16 | permission_classes = (IsAuthenticated,) 17 | 18 | 19 | class BaseGetApiView(BaseAuthView, permissions.BasePermission): 20 | def has_permission(self, request, view): 21 | if request.method == 'GET': 22 | return True 23 | return request.user and request.user.is_authenticated() 24 | 25 | 26 | class BaseDestroyApiView(BaseAuthView): 27 | 28 | def has_permission(self, request, view): 29 | if request.method == 'DELETE': 30 | return True 31 | return request.user and request.user.is_authenticated() 32 | 33 | 34 | class BasePostApiView(BaseAuthView): 35 | 36 | def has_permission(self, request, view): 37 | if request.method == 'POST': 38 | return True 39 | return request.user and request.user.is_authenticated() 40 | 41 | 42 | class BasePutApiView(BaseAuthView): 43 | 44 | def has_permission(self, request, view): 45 | if request.method == 'PUT': 46 | return True 47 | return request.user and request.user.is_authenticated() 48 | 49 | 50 | class BaseUpdateApiView(BaseAuthView): 51 | 52 | def has_permission(self, request, view): 53 | if request.method == 'POST' or request.method == 'PUT' or request.method == 'PATCH': 54 | return True 55 | return request.user and request.user.is_authenticated() 56 | 57 | 58 | class BaseUpdateAndDestroyApiView(BaseAuthView): 59 | 60 | def has_permission(self, request, view): 61 | if request.method == 'POST' or request.method == 'PUT' or request.method == 'PATCH' or request.method == 'DELETE': 62 | return True 63 | return request.user and request.user.is_authenticated() 64 | 65 | 66 | class BaseCRUDApiView(BaseAuthView): 67 | 68 | def has_permission(self, request, view): 69 | if request.method == 'POST' or request.method == 'PUT' or request.method == 'PATCH' or request.method == 'GET': 70 | return True 71 | return request.user and request.user.is_authenticated() 72 | 73 | -------------------------------------------------------------------------------- /project_name/api/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /project_name/api/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /project_name/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | api_url_pattern = [ 4 | 5 | ] 6 | -------------------------------------------------------------------------------- /project_name/api/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /project_name/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-boilerplate/35ed124d3290417037dcb382936eebe62dd2f13f/project_name/core/__init__.py -------------------------------------------------------------------------------- /project_name/core/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | """ 4 | Package that allow the whole project to use common functions as a resource 5 | """ 6 | 7 | class TimeStampedModel(models.Model): 8 | """ 9 | An Abstract Base Class provides self-updating `create` and `modified` fields, for every model 10 | """ 11 | created = models.DateTimeField(auto_now_add=True) 12 | modified = models.DateTimeField(auto_now=True) 13 | 14 | class Meta: 15 | abstract = True 16 | -------------------------------------------------------------------------------- /project_name/core/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-boilerplate/35ed124d3290417037dcb382936eebe62dd2f13f/project_name/core/tests/__init__.py -------------------------------------------------------------------------------- /project_name/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-boilerplate/35ed124d3290417037dcb382936eebe62dd2f13f/project_name/lib/__init__.py -------------------------------------------------------------------------------- /project_name/lib/audit/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008, 2009 James Aylett 2 | Copyright (c) 2008 Aaron Sokoloski 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /project_name/lib/audit/README.txt: -------------------------------------------------------------------------------- 1 | ==================== 2 | django_audited_model 3 | ==================== 4 | 5 | Django middleware to remember the current user (from the session) during a request, and use it to wire up created_by and modified_by fields. Also a pair of abstract models so you don't have to type the field definitions out yourself. 6 | 7 | Use 8 | === 9 | 10 | Add ``audited_model.middleware.AutoCreatedAndModifiedFields`` to your MIDDLEWARE_CLASSES. Then add suitable fields to any models that need them: 11 | 12 | Managed on creation only 13 | ------------------------ 14 | 15 | created_by = models.ForeignKey(User, related_name='created_%(class)s_set', null=False, blank=True) 16 | 17 | You may also like to have a second field to record the creation time: 18 | 19 | created_at = models.DateTimeField(null=False, blank=False, auto_now_add=True) 20 | 21 | Managed on any modification 22 | --------------------------- 23 | 24 | modified_by = models.ForeignKey(User, related_name='modified_%(class)s_set', null=False, blank=True) 25 | 26 | Similarly, a second field to record the modification time may be helpful: 27 | 28 | modified_at = models.DateTimeField(null=False, blank=False, auto_now=True) 29 | 30 | "Loose" auditing 31 | ================ 32 | 33 | If modified_by and/or created_by are nullable, by setting null=True, blank=True in the field definition, then if there is no authenticated session user they will be left unset. If they are *not* nullable, Django will raise an exception on save. 34 | 35 | Convenience abstract models 36 | =========================== 37 | 38 | To avoid typing the field definitions in, inherit from AuditedModel or LooselyAuditedModel as required. These contain all four fields above. 39 | 40 | Admin 41 | ===== 42 | 43 | You may prefer to exclude the above fields from the admin interface, for clarity. This is *not* set up automatically by the abstract models; however audited_model.models.exclude is a list of these fields. 44 | 45 | Use in the shell and from management commands 46 | ============================================= 47 | 48 | Middleware is not initialised or run from the ./manage.py shell or from management commands. If you need this behaviour in these cases, make sure you do the following: 49 | 50 | ---------------------------------------------------------------------------- 51 | from audited_model.middleware import set_current_user 52 | ---------------------------------------------------------------------------- 53 | 54 | This will set up the pre-save hook which manages the fields. If you are using strict (non-nullable) fields, you will also need to set the current user: 55 | 56 | ---------------------------------------------------------------------------- 57 | set_current_user(u) 58 | ---------------------------------------------------------------------------- 59 | 60 | For these purposes it is often useful to have a "system" user with a low pk, so you can do something like: 61 | 62 | ---------------------------------------------------------------------------- 63 | from django.contrib.auth.models import User 64 | set_current_user(User.objects.get(pk=1)) 65 | ---------------------------------------------------------------------------- 66 | 67 | Other notes 68 | =========== 69 | 70 | The model shortcuts were originally written by Aaron Sokoloski, with some bugfixes by Andrew Godwin. The whole thing has been created and maintained at . 71 | 72 | James Aylett 73 | -------------------------------------------------------------------------------- /project_name/lib/audit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-boilerplate/35ed124d3290417037dcb382936eebe62dd2f13f/project_name/lib/audit/__init__.py -------------------------------------------------------------------------------- /project_name/lib/audit/middleware.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2009 James Aylett 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from django.db.models.signals import pre_save 22 | import threading 23 | 24 | stash = threading.local() 25 | 26 | def get_current_user(): 27 | """Get the user whose session resulted in the current code running. (Only valid during requests.)""" 28 | return getattr(stash, 'current_user', None) 29 | 30 | def set_current_user(user): 31 | stash.current_user = user 32 | 33 | def onanymodel_presave(sender, **kwargs): 34 | current_user = get_current_user() 35 | if current_user is None or current_user.is_anonymous(): 36 | # if there is no current user or we're an anonymous user (ie: guest) then 37 | # don't do anything. The save() will fail if created_by or modified_by are 38 | # null=False, and not otherwise; ie the behaviour is controlled by the 39 | # modelos, as desired. 40 | current_user = None 41 | 42 | obj = kwargs['instance'] 43 | if hasattr(obj, 'modified_by_id') and current_user: 44 | obj.modified_by = current_user 45 | if not obj.pk and current_user and hasattr(obj, 'created_by_id'): 46 | try: 47 | if not obj.created_by: 48 | obj.created_by = current_user 49 | except obj.__class__.created_by.field.rel.to.DoesNotExist: 50 | # FRAGILE: reliant on Django internals 51 | # (django.db.modelos.fields.related.ReverseSingleRelatedObjectDescriptor and down) 52 | # 53 | # however will work if you don't use the django auth system, and make the created_by 54 | # field a ForeignKey to whatever you use instead of django.contrib.auth.modelos.User. 55 | obj.created_by = current_user 56 | 57 | pre_save.connect(onanymodel_presave) 58 | 59 | class AutoCreatedAndModifiedFields: 60 | def process_request(self, request): 61 | set_current_user(request.user) 62 | def process_response(self, request, response): 63 | set_current_user(None) 64 | return response 65 | def process_exception(self, request, exception): 66 | set_current_user(None) 67 | -------------------------------------------------------------------------------- /project_name/lib/audit/models.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2009 James Aylett 2 | # Copyright (c) 2008 Aaron Sokoloski 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | from django.conf import settings 23 | from django.db import models 24 | 25 | 26 | exclude = ['created_by', 'modified_by'] 27 | 28 | 29 | class AuditedModel(models.Model): 30 | created_at = models.DateTimeField(null=False, blank=False, auto_now_add=True) 31 | created_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='created_%(class)s_set', null=False, blank=True) 32 | modified_at = models.DateTimeField(null=False, blank=False, auto_now=True) 33 | modified_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='modified_%(class)s_set', null=False, blank=True, on_delete=models.SET_DEFAULT, default=1) 34 | 35 | class Meta: 36 | abstract = True 37 | 38 | 39 | class LooselyAuditedModel(models.Model): 40 | created_at = models.DateTimeField(null=False, blank=False, auto_now_add=True) 41 | created_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='created_%(class)s_set', null=True, blank=True) 42 | modified_at = models.DateTimeField(null=False, blank=False, auto_now=True) 43 | modified_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='modified_%(class)s_set', null=True, blank=True, on_delete=models.SET_DEFAULT, default=1) 44 | 45 | class Meta: 46 | abstract = True 47 | -------------------------------------------------------------------------------- /project_name/lib/cache/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-boilerplate/35ed124d3290417037dcb382936eebe62dd2f13f/project_name/lib/cache/__init__.py -------------------------------------------------------------------------------- /project_name/lib/cache/decorators.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | 4 | class memoize(object): 5 | """ 6 | Cache return value of an instance method 7 | This class is meant to be used as a decorator of methods. The return value 8 | from a given method invocation will be cached on the instance whose method 9 | was invoked. All arguments passed to a method decorated with memoize must 10 | be hashable. 11 | Author: http://code.activestate.com/recipes/577452-a-memoize-decorator-for-instance-methods/ 12 | """ 13 | 14 | def __init__(self, func): 15 | self.func = func 16 | 17 | def __get__(self, obj, objtype=None): 18 | if obj is None: 19 | return self.func 20 | return functools.partial(self, obj) 21 | 22 | def __call__(self, *args, **kwargs): 23 | obj = args[0] 24 | if not hasattr(obj, '_cache'): 25 | obj._cache = {} 26 | 27 | key = (self.func, args[1:], frozenset(kwargs.items())) 28 | if key not in obj._cache: 29 | obj._cache[key] = self.func(*args, **kwargs) 30 | 31 | return obj._cache[key] 32 | 33 | 34 | class memoize_invalidate(object): 35 | """ 36 | Use in conjunction with @memoize decorator when you want to reset instance cache 37 | when calling a specific method 38 | """ 39 | 40 | def __init__(self, func): 41 | self.func = func 42 | 43 | def __get__(self, obj, objtype=None): 44 | if obj is None: 45 | return self.func 46 | return functools.partial(self, obj) 47 | 48 | def __call__(self, *args, **kwargs): 49 | obj = args[0] 50 | obj._cache = {} 51 | return self.func(*args, **kwargs) 52 | -------------------------------------------------------------------------------- /project_name/lib/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-boilerplate/35ed124d3290417037dcb382936eebe62dd2f13f/project_name/lib/common/__init__.py -------------------------------------------------------------------------------- /project_name/lib/common/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-boilerplate/35ed124d3290417037dcb382936eebe62dd2f13f/project_name/lib/common/templatetags/__init__.py -------------------------------------------------------------------------------- /project_name/lib/common/templatetags/dateutils.py: -------------------------------------------------------------------------------- 1 | from django.template import Library, TemplateSyntaxError 2 | 3 | 4 | register = Library() 5 | 6 | 7 | @register.filter 8 | def week_number(value): 9 | try: 10 | return value.strftime('%W') 11 | except AttributeError: 12 | raise TemplateSyntaxError('week_number filter must be passed a datetime-like object!') 13 | -------------------------------------------------------------------------------- /project_name/lib/common/templatetags/dicthelpers.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.filter(name="getkey") 7 | def getkey(value, key): 8 | """\ 9 | Fetch a key from a value. Useful for cases such as dicts which have '-' and ' ' 10 | in the key name, which Django can't cope with via the standard '.' lookup. 11 | """ 12 | try: 13 | return value[key] 14 | except KeyError: 15 | return "" 16 | 17 | 18 | @register.filter(name="delkey") 19 | def delkey(value, key): 20 | """ 21 | :param value: dictionary 22 | :param key: string - name of element to delete 23 | """ 24 | value.pop(key) 25 | return "" 26 | -------------------------------------------------------------------------------- /project_name/lib/common/templatetags/formhelpers.py: -------------------------------------------------------------------------------- 1 | from django.template import Library 2 | from django import forms 3 | 4 | register = Library() 5 | 6 | 7 | def get_current_choice_field_value(bound_field): 8 | choices = list(bound_field.field.choices) 9 | current_value = bound_field.value() 10 | 11 | try: 12 | selected_choice = [label for value, label in choices if str(value) == str(current_value)][0] 13 | return selected_choice 14 | except IndexError: 15 | return bound_field.label 16 | 17 | 18 | @register.assignment_tag(name='get_field_text_value') 19 | @register.simple_tag 20 | def field_text_value(bound_field): 21 | if isinstance(bound_field.field, forms.ChoiceField): 22 | return get_current_choice_field_value(bound_field) 23 | 24 | elif isinstance(bound_field.field, forms.CharField): 25 | return bound_field.value() 26 | 27 | return bound_field.label 28 | 29 | @register.filter 30 | def fieldtype(obj): 31 | return obj.__class__.__name__ -------------------------------------------------------------------------------- /project_name/lib/common/templatetags/list_helpers.py: -------------------------------------------------------------------------------- 1 | from django.template import Library 2 | 3 | register = Library() 4 | 5 | 6 | @register.filter 7 | def pluck(items, attr): 8 | def pluck_attr(item): 9 | if hasattr(item, 'get'): 10 | return item.get(attr) 11 | 12 | if hasattr(item, attr): 13 | return item.attr 14 | 15 | raise ValueError('Item `%s` has no attribute `%s`' % (item, attr)) 16 | 17 | return [ 18 | pluck_attr(item) for item in items 19 | ] 20 | 21 | 22 | @register.filter(name='sum') 23 | def sum_items(items): 24 | return sum(items) 25 | 26 | 27 | @register.filter(name='groupby') 28 | def group_by(items, number): 29 | return [ 30 | items[index:index + number] for index in xrange(0, len(items), number) 31 | ] 32 | 33 | 34 | @register.filter(name='split') 35 | def split_list(items, size): 36 | output = [[] for x in xrange(0, size)] 37 | 38 | for index, item in enumerate(items): 39 | output[index % size].append(item) 40 | 41 | return output 42 | 43 | 44 | @register.filter 45 | def lookup(d, key): 46 | return d[key] -------------------------------------------------------------------------------- /project_name/lib/common/templatetags/numberhelpers.py: -------------------------------------------------------------------------------- 1 | from math import floor 2 | from django import template 3 | from django.template.defaultfilters import floatformat 4 | 5 | import numbers 6 | 7 | register = template.Library() 8 | 9 | 10 | @register.filter_function 11 | def humanise_number(number, decimals=1): 12 | digits = number 13 | suffix = None 14 | 15 | if not isinstance(number, numbers.Number): 16 | return str(number) 17 | 18 | if number < 1000: 19 | return str(number) 20 | 21 | number = float(number) 22 | 23 | if number >= 1000: 24 | digits = floored_division(number, 1000.0, decimals) 25 | suffix = 'K' 26 | if number >= 1000000: 27 | digits = floored_division(number, 1000000.0, decimals) 28 | suffix = 'M' 29 | 30 | return "{:.{precision}f}{suffix}+".format(digits, precision=decimals, suffix=suffix) 31 | 32 | 33 | def floored_division(numerator, divisor, digits): 34 | val = (numerator / divisor) * 10 ** digits 35 | return floor(val) / 10 ** digits 36 | 37 | 38 | @register.filter 39 | def percent(value, decimal_places=0): 40 | if value is None: 41 | return None 42 | try: 43 | value = float(value) 44 | except ValueError: 45 | return None 46 | return floatformat(value * 100.0, decimal_places) + '%' 47 | -------------------------------------------------------------------------------- /project_name/lib/common/templatetags/sitemaphelpers.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django import template 3 | 4 | register = template.Library() 5 | 6 | 7 | @register.simple_tag(takes_context=True) 8 | def get_sitemap_href(context): 9 | request = context.get('request') 10 | if request: 11 | host = request.get_host() 12 | else: 13 | host = settings.CANONICAL_SITE_URL 14 | return 'https://%s/sitemap.xml' % host 15 | 16 | 17 | @register.inclusion_tag('_meta/language_alternatives.html', takes_context=True) 18 | def get_language_alternatives(context): 19 | 20 | lang_domains = [] 21 | for language, host in settings.AF_SITEMAPS: 22 | lang_domains.append({'lang': language, 'host': host}) 23 | return { 24 | 'request': context.get('request'), 25 | 'lang_domains': lang_domains 26 | } 27 | -------------------------------------------------------------------------------- /project_name/lib/common/templatetags/statichelpers.py: -------------------------------------------------------------------------------- 1 | from django.template import Library 2 | from django.contrib.staticfiles.storage import staticfiles_storage 3 | 4 | register = Library() 5 | 6 | 7 | @register.assignment_tag(name='get_static_path') 8 | def static_path(path): 9 | return staticfiles_storage.url(path) 10 | -------------------------------------------------------------------------------- /project_name/lib/common/templatetags/stringhelpers.py: -------------------------------------------------------------------------------- 1 | import re 2 | from django import template 3 | 4 | from django.utils.html import escape 5 | from django.utils.safestring import mark_safe 6 | 7 | register = template.Library() 8 | 9 | is_char = re.compile(r"[a-zA-Z0-9]") 10 | 11 | nbsp_regex = re.compile(r' ') 12 | htmle_regex = re.compile(r'&[a-zA-Z0-9]{2,8};') 13 | link_regex = re.compile(r'(([a-zA-Z]+)://[^ \t\n\r]+)', re.MULTILINE) 14 | 15 | @register.filter_function 16 | def truncatechars_nospace(string, length): 17 | string = string.rstrip() 18 | 19 | if len(string) <= length: 20 | return string 21 | 22 | ret = string[:length].rstrip() 23 | 24 | if ret[-1:][0] in ",": 25 | ret = ret[:-1] 26 | 27 | elif is_char.match(ret[-1:][0]): 28 | ret += "..." 29 | 30 | return ret 31 | 32 | 33 | @register.filter_function 34 | def no_trailing_ellipsis(string): 35 | if string.endswith('...'): 36 | return string[:3] 37 | 38 | return string 39 | 40 | @register.filter_function 41 | def append(string1, string2): 42 | return "%s%s" % (string1, string2) 43 | 44 | 45 | @register.filter_function 46 | def strip_html_entities(string): 47 | 48 | string = re.sub(nbsp_regex, ' ', string) 49 | string = re.sub(htmle_regex, '', string) 50 | 51 | return string 52 | 53 | 54 | @register.filter() 55 | def htmlentities(s): 56 | return mark_safe(escape(s).encode('ascii', 'xmlcharrefreplace')) 57 | 58 | 59 | @register.filter_function 60 | def linkify(value): 61 | 62 | def _spacify(s, chars=100): 63 | if len(s) <= chars: 64 | return s 65 | for k in range(len(s) / chars): 66 | pos = (k + 1) * chars 67 | s = s[0:pos] + ' ' + s[pos:] 68 | return s 69 | 70 | def _replace(match): 71 | href = match.group(0) 72 | return '%s' % (href, _spacify(href)) 73 | 74 | return link_regex.sub(_replace, value) 75 | -------------------------------------------------------------------------------- /project_name/lib/common/templatetags/templatehelpers.py: -------------------------------------------------------------------------------- 1 | import re 2 | import random 3 | 4 | from django.template import Library 5 | from django.template.loader import render_to_string, get_template 6 | from django.utils.safestring import mark_safe 7 | 8 | register = Library() 9 | 10 | 11 | SPACE_REGEX = re.compile('\s+') 12 | 13 | 14 | @register.simple_tag(takes_context=True) 15 | def render_stripped_contents(context, template): 16 | rendered = render_to_string(template, context) 17 | 18 | return re.sub(SPACE_REGEX, ' ', rendered) 19 | 20 | 21 | @register.simple_tag(takes_context=True) 22 | def render_as_template(context, string): 23 | template = get_template(string) 24 | return mark_safe( 25 | template.render(context) 26 | ) 27 | 28 | 29 | @register.assignment_tag 30 | def get_random_integer(minimum, maximum): 31 | return random.randint(minimum, maximum) 32 | -------------------------------------------------------------------------------- /project_name/lib/common/templatetags/urlhelpers.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.conf import settings 4 | from django import template 5 | from django.contrib.staticfiles.storage import staticfiles_storage 6 | from django.utils.http import urlquote 7 | import lib.html.utils 8 | 9 | register = template.Library() 10 | 11 | 12 | @register.simple_tag 13 | def media(path): 14 | "Template tag which inserts the relevant prefix for the media files" 15 | return u"%s%s" % (settings.MEDIA_URL, urlquote(path)) 16 | 17 | 18 | @register.simple_tag 19 | def minified_static(path): 20 | """ 21 | A template tag that returns the minified URL to a file 22 | using staticfiles' storage backend. If we are in debugging 23 | mode, this will return the non-minified version. 24 | """ 25 | 26 | if settings.DEBUG: 27 | return staticfiles_storage.url(path) 28 | split_path = os.path.splitext(path) 29 | new_path = "%s.min%s" % (split_path[0], split_path[1]) 30 | return staticfiles_storage.url(new_path) 31 | 32 | 33 | @register.filter 34 | def absolutize_url(url): 35 | return lib.html.utils.absolutize_link(url) 36 | -------------------------------------------------------------------------------- /project_name/lib/common/utils.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | import django.core.urlresolvers 4 | from django.conf import settings 5 | 6 | 7 | """\ 8 | A wrapper for reverse that always sets the URLConf to the {{ project_name }}. 9 | This is safe to use from the API, unlike the normal reverse(). 10 | """ 11 | reverse = functools.partial( 12 | django.core.urlresolvers.reverse, urlconf=settings.ROOT_URLCONF) 13 | -------------------------------------------------------------------------------- /project_name/lib/common/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponseForbidden, HttpResponseRedirect 2 | from lib.common.utils import reverse 3 | from django.contrib.auth.mixins import LoginRequiredMixin 4 | 5 | 6 | class StaffOnlyMixin(object): 7 | 8 | def dispatch(self, request, *args, **kwargs): 9 | if not request.user.is_staff: 10 | return HttpResponseForbidden() 11 | return super(StaffOnlyMixin, self).dispatch(request, *args, **kwargs) 12 | 13 | 14 | class UserAuthMixin(object): 15 | 16 | def dispatch(self, request, *args, **kwargs): 17 | if not request.user.is_authenticated(): 18 | return HttpResponseRedirect(reverse('login')) 19 | return super(UserAuthMixin, self).dispatch(request, *args, **kwargs) 20 | 21 | 22 | class BaseTemplateMixin(LoginRequiredMixin): 23 | 24 | def get_context_data(self, **kwargs): 25 | context = super(BaseTemplateMixin, self).get_context_data(**kwargs) 26 | context.update({ 27 | 'active': False 28 | }) 29 | return context 30 | 31 | -------------------------------------------------------------------------------- /project_name/lib/datetime/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-boilerplate/35ed124d3290417037dcb382936eebe62dd2f13f/project_name/lib/datetime/__init__.py -------------------------------------------------------------------------------- /project_name/lib/datetime/utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | class ConvertedDateTime: 4 | """ 5 | DateTime object parser 6 | """ 7 | 8 | def __init__(self, date_str): 9 | self._date = date_str 10 | 11 | def convert_date(self, format_str): 12 | """ 13 | Given a format, returns a datetime object 14 | """ 15 | #TODO: Later, this function should receive the client timezone and based on that, determine the format 16 | return datetime.datetime.strptime(self._date, format_str) -------------------------------------------------------------------------------- /project_name/lib/forms/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-boilerplate/35ed124d3290417037dcb382936eebe62dd2f13f/project_name/lib/forms/__init__.py -------------------------------------------------------------------------------- /project_name/lib/forms/generic.py: -------------------------------------------------------------------------------- 1 | from django.http import Http404 2 | from django.views.generic.edit import FormMixin 3 | from django.views.generic.list import ListView 4 | 5 | 6 | class FormListView(ListView, FormMixin): 7 | def get(self, request, *args, **kwargs): 8 | form_class = self.get_form_class() 9 | self.form = self.get_form(form_class) 10 | 11 | self.object_list = self.get_queryset() 12 | allow_empty = self.get_allow_empty() 13 | if not allow_empty and len(self.object_list) == 0: 14 | raise Http404(_(u"Empty list and '%(class_name)s.allow_empty' is False.") 15 | % {'class_name': self.__class__.__name__}) 16 | 17 | context = self.get_context_data(object_list=self.object_list, form=self.form) 18 | return self.render_to_response(context) 19 | 20 | def post(self, request, *args, **kwargs): 21 | return self.get(request, *args, **kwargs) 22 | -------------------------------------------------------------------------------- /project_name/lib/html/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /project_name/lib/html/markdown_extensions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Based on: https://github.com/bhch/markdown-nofollow 3 | """ 4 | import re 5 | from markdown import Extension 6 | from markdown.inlinepatterns import LinkPattern, LINK_RE 7 | from django.conf import settings 8 | 9 | BASE_URL = "^(https|http)://%s" % settings.CANONICAL_SITE_URL 10 | 11 | 12 | def is_href_external(href): 13 | if not href: 14 | return False 15 | if href.startswith('/') or re.match(BASE_URL, href): 16 | return False 17 | return True 18 | 19 | 20 | class NofollowMixin(object): 21 | 22 | def handleMatch(self, m): 23 | element = super(NofollowMixin, self).handleMatch(m) 24 | if element is not None and is_href_external(element.get('href')): 25 | element.set('rel', 'nofollow') 26 | return element 27 | 28 | 29 | class TargetBlankMixin(object): 30 | 31 | def handleMatch(self, m): 32 | element = super(TargetBlankMixin, self).handleMatch(m) 33 | if element is not None and is_href_external(element.get('href')): 34 | element.set('target', '_blank') 35 | return element 36 | 37 | 38 | class ExternalLinkPattern(NofollowMixin, TargetBlankMixin, LinkPattern): 39 | pass 40 | 41 | 42 | class ExternalLinkExtensions(Extension): 43 | 44 | def extendMarkdown(self, md, md_globals): 45 | md.inlinePatterns['link'] = ExternalLinkPattern(LINK_RE, md) 46 | -------------------------------------------------------------------------------- /project_name/lib/html/utils.py: -------------------------------------------------------------------------------- 1 | import markdown 2 | from bs4 import BeautifulSoup 3 | import premailer 4 | 5 | from django.conf import settings 6 | from lib.html.markdown_extensions import ExternalLinkExtensions 7 | 8 | 9 | def absolutize_email_content(content): 10 | content = content.strip() 11 | 12 | if not len(content): 13 | return '' 14 | 15 | base_url = "https://%s" % settings.CANONICAL_SITE_URL 16 | transformed_content = premailer.transform(content, base_url=base_url) 17 | 18 | soup = BeautifulSoup.BeautifulSoup(transformed_content) 19 | 20 | return ''.join([unicode(x) for x in soup.body.contents]) 21 | 22 | 23 | def absolutize_link(link): 24 | protocol = 'http' 25 | if settings.SECURE_SSL_REDIRECT: 26 | protocol = 'https' 27 | return "{protocol}://{host}{link}".format( 28 | protocol=protocol, host=settings.CANONICAL_SITE_URL, link=link 29 | ) 30 | 31 | 32 | def get_html_from_markdown(markdown_content, strip_paragraph=False): 33 | if markdown_content: 34 | content = markdown.markdown(markdown_content, extensions=[ExternalLinkExtensions()]) 35 | else: 36 | return u"" 37 | 38 | content = content.strip() 39 | if not strip_paragraph: 40 | return content 41 | 42 | if content.startswith('

') and content.endswith('

'): 43 | return content[3:-4] 44 | return content 45 | -------------------------------------------------------------------------------- /project_name/lib/storage.py: -------------------------------------------------------------------------------- 1 | import django.contrib.staticfiles.storage 2 | 3 | 4 | class CachedStaticFilesStorage( 5 | django.contrib.staticfiles.storage.CachedStaticFilesStorage): 6 | 7 | def hashed_name(self, name, content=None): 8 | try: 9 | return super(CachedStaticFilesStorage, self).hashed_name( 10 | name, content=content) 11 | except ValueError: 12 | # If there is a static asset that refers to another asset 13 | # that doesn't exist, don't fall over, just ignore it. 14 | return name 15 | 16 | 17 | class DatabaseRouter(object): 18 | """ 19 | Determine how to route database calls for an app's modelos (in this case, for an app named Example). 20 | All other modelos will be routed to the next router in the DATABASE_ROUTERS setting if applicable, 21 | or otherwise to the default database. 22 | """ 23 | 24 | def db_for_read(self, model, **hints): 25 | """Send all read operations on Example app modelos to `example_db`.""" 26 | if model._meta.app_label == 'example': 27 | return 'example_db' 28 | return None 29 | 30 | def db_for_write(self, model, **hints): 31 | """Send all write operations on Example app modelos to `example_db`.""" 32 | if model._meta.app_label == 'example': 33 | return 'example_db' 34 | return None 35 | 36 | def allow_relation(self, obj1, obj2, **hints): 37 | """Determine if relationship is allowed between two objects.""" 38 | 39 | # Allow any relation between two modelos that are both in the Example app. 40 | if obj1._meta.app_label == 'example' and obj2._meta.app_label == 'example': 41 | return True 42 | # No opinion if neither object is in the Example app (defer to default or other routers). 43 | elif 'example' not in [obj1._meta.app_label, obj2._meta.app_label]: 44 | return None 45 | 46 | # Block relationship if one object is in the Example app and the other isn't. 47 | return False 48 | 49 | def allow_migrate(self, db, app_label, model_name=None, **hints): 50 | """Ensure that the Example app's modelos get created on the right database.""" 51 | if app_label == 'example': 52 | # The Example app should be migrated only on the example_db database. 53 | return db == 'example_db' 54 | elif db == 'example_db': 55 | # Ensure that all other apps don't get migrated on the example_db database. 56 | return False 57 | 58 | # No opinion for all other scenarios 59 | return None 60 | -------------------------------------------------------------------------------- /project_name/lib/viewslibrary/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-boilerplate/35ed124d3290417037dcb382936eebe62dd2f13f/project_name/lib/viewslibrary/__init__.py -------------------------------------------------------------------------------- /project_name/lib/viewslibrary/tests.py: -------------------------------------------------------------------------------- 1 | import urllib.parse 2 | 3 | from django import http 4 | 5 | 6 | class TestRequest(http.HttpRequest): 7 | """Just enough implementation to make it testable""" 8 | 9 | def __init__(self, uri, method='GET'): 10 | super(TestRequest, self).__init__() 11 | self.method = method 12 | _, self.host, self.path, self.query, _ = urllib.parse.urlsplit(uri) 13 | self.META['QUERY_STRING'] = self.query 14 | self.GET = http.QueryDict(self.query) 15 | 16 | def get_host(self): 17 | return self.host 18 | -------------------------------------------------------------------------------- /project_name/lib/viewslibrary/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django import http 4 | from django.views.generic import FormView, UpdateView, CreateView, ListView 5 | from django.core.paginator import InvalidPage 6 | from django.core.urlresolvers import NoReverseMatch 7 | from django.core.urlresolvers import reverse, resolve 8 | from django.utils.functional import LazyObject 9 | from django.core.serializers.json import DjangoJSONEncoder 10 | from django.http.response import HttpResponse 11 | from lib.forms.generic import FormListView 12 | 13 | 14 | 15 | class AjaxTemplateMixin(object): 16 | """ Vary the template on request.is_ajax. 17 | TODO: Also attempt to call ajax_get() etc instead of the usual 18 | ones if they are present? 19 | """ 20 | 21 | def dispatch(self, request, **kwargs): 22 | if request.is_ajax(): 23 | self.template_name = self.ajax_template_name 24 | return super(AjaxTemplateMixin, self).dispatch(request, **kwargs) 25 | 26 | 27 | class AjaxLoadMoreMixin(object): 28 | """ Mixin for providing AJAX pagination facilities to any 29 | view that takes a "page" kwarg and provides a "page" variable in it's context data 30 | """ 31 | 32 | def get(self, request, **kwargs): 33 | response = super(AjaxLoadMoreMixin, self).get(request, **kwargs) 34 | if request.is_ajax(): 35 | json_data = {"content": response.render().content} 36 | 37 | try: 38 | next_page = response.context_data["page_obj"].next_page_number() 39 | if next_page: 40 | kwargs = dict(self.kwargs) 41 | kwargs["page"] = next_page 42 | try: 43 | json_data["next_url"] = reverse(resolve(request.path).url_name, args=self.args, kwargs=kwargs) 44 | except NoReverseMatch: 45 | # this results in there not being a Next button 46 | json_data["next_url"] = None 47 | except InvalidPage: 48 | pass 49 | # response = JsonResponse(json, status_code=200) 50 | return response 51 | 52 | 53 | class AjaxView(object): 54 | FORM_INVALID_HTTP_STATUS_CODE = 400 55 | ''' 56 | A specialised version of the Django FormView that provides default 57 | JSON responses for AJAX requests. 58 | ''' 59 | 60 | def form_valid(self, form): 61 | ''' 62 | Calls the appropriate handler depending on whether or not we're 63 | dealing with an AJAX request. If you would subclass this when 64 | using a normal FormView, you should subclass default_form_valid 65 | or ajax_form_valid instead. 66 | ''' 67 | if self.request.is_ajax(): 68 | return self.ajax_form_valid(form) 69 | else: 70 | return self.default_form_valid(form) 71 | 72 | def ajax_form_valid(self, form): 73 | ''' 74 | Handles AJAX submissions with valid data by responding with a 75 | 200 (OK) with a JSON body. 76 | ''' 77 | return JsonResponse( 78 | {'result': 'ok'}, 79 | status_code=200, 80 | ) 81 | 82 | def default_form_valid(self, form): 83 | ''' 84 | Handles non-AJAX submissions with valid data by calling FormView's 85 | implemenation of form_valid. 86 | ''' 87 | return super().form_valid(form) 88 | 89 | def form_invalid(self, form): 90 | ''' 91 | Calls the appropriate handler depending on whether or not we're 92 | dealing with an AJAX request. If you would subclass this when 93 | using a normal FormView, you should subclass default_form_invalid 94 | or ajax_form_invalid instead. 95 | ''' 96 | if self.request.is_ajax(): 97 | return self.ajax_form_invalid(form) 98 | else: 99 | return self.default_form_invalid(form) 100 | 101 | def ajax_form_invalid(self, form): 102 | ''' 103 | Handles AJAX submissions with invalid data and responds with a 104 | 400 (bad request) with a JSON body containing errors and the form's 105 | prefix. 106 | ''' 107 | return JsonResponse( 108 | { 109 | 'result': 'error', 110 | 'errors': form.errors, 111 | 'prefix': form.add_prefix(''), 112 | }, 113 | status_code=self.FORM_INVALID_HTTP_STATUS_CODE, 114 | ) 115 | 116 | def default_form_invalid(self, form): 117 | ''' 118 | Handles non-AJAX submissions with invalid data by calling FormView's 119 | implementation of form_invalid. 120 | ''' 121 | return super().form_invalid(form) 122 | 123 | 124 | class AjaxFormView(AjaxView, FormView): 125 | pass 126 | 127 | 128 | class AjaxUpdateView(AjaxView, UpdateView): 129 | pass 130 | 131 | 132 | class AjaxCreateView(AjaxView, CreateView): 133 | pass 134 | 135 | 136 | class AjaxListView(AjaxView, FormListView): 137 | pass 138 | 139 | 140 | class AjaxFormDataView(AjaxFormView): 141 | FORM_INVALID_HTTP_STATUS_CODE = 200 142 | 143 | def get_form_kwargs(self): 144 | if not self.request.is_ajax(): 145 | return super(AjaxFormDataView, self).get_form_kwargs() 146 | form_kwargs = super(AjaxFormDataView, self).get_form_kwargs() 147 | request_body = self.request.read() 148 | try: 149 | data = json.loads(request_body) 150 | except (ValueError, TypeError): 151 | data = {} 152 | form_kwargs['data'] = data 153 | return form_kwargs 154 | 155 | 156 | class JsonResponse(HttpResponse): 157 | 158 | def __init__(self, data, url=None, encoder=DjangoJSONEncoder, safe=True, 159 | json_dumps_params=None, **kwargs): 160 | if safe and not isinstance(data, dict): 161 | raise TypeError( 162 | 'In order to allow non-dict objects to be serialized set the ' 163 | 'safe parameter to False.' 164 | ) 165 | if json_dumps_params is None: 166 | json_dumps_params = {} 167 | kwargs.setdefault('content_type', 'application/json') 168 | data.update( 169 | {'url': url} 170 | ) 171 | data = json.dumps(data, cls=encoder, **json_dumps_params) 172 | super().__init__(content=data, **kwargs) 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /project_name/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /project_name/project_name/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-boilerplate/35ed124d3290417037dcb382936eebe62dd2f13f/project_name/project_name/__init__.py -------------------------------------------------------------------------------- /project_name/project_name/celery.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from celery import Celery 4 | from django.conf import settings 5 | 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings") 7 | 8 | 9 | CELERY_TIMEZONE = 'UTC' 10 | 11 | app = Celery('{{ project_name }}') 12 | app.config_from_object('django.conf:settings') 13 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) 14 | -------------------------------------------------------------------------------- /project_name/project_name/databases.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 4 | 5 | BD_NON_DEFAULT = u'db_non_default' 6 | 7 | DATABASES = { 8 | 'default': { 9 | 'ENGINE': 'django.db.backends.sqlite3', 10 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 11 | }, 12 | # BD_NON_DEFAULT: { 13 | # 'ENGINE': 'django.db.backends.postgresql_psycopg2', 14 | # 'NAME': 'postgres', 15 | # 'USER': 'postgres', 16 | # 'PASSWORD': 'postgres', 17 | # 'HOST': 'postgres', 18 | # 'PORT': '5432', 19 | # } 20 | } 21 | -------------------------------------------------------------------------------- /project_name/project_name/development/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-boilerplate/35ed124d3290417037dcb382936eebe62dd2f13f/project_name/project_name/development/__init__.py -------------------------------------------------------------------------------- /project_name/project_name/development/settings.py: -------------------------------------------------------------------------------- 1 | from {{ project_name }}.settings import * 2 | 3 | # To see outgoing email dumped to a terminal, uncomment the following and 4 | # run "python -m smtpd -n -c DebuggingServer localhost:1025" 5 | DEBUG = True 6 | 7 | DJANGOENV = 'development' 8 | 9 | try: 10 | import readline 11 | readline # make pyflakes happy, readline makes interactive mode keep history 12 | except ImportError: 13 | # no readline on Windows 14 | pass 15 | 16 | MIDDLEWARE += [ 17 | 'debug_toolbar.middleware.DebugToolbarMiddleware' 18 | ] 19 | 20 | INSTALLED_APPS += [ 21 | 'django_nose', 22 | 'django_extensions', 23 | 'debug_toolbar', 24 | 'template_repl', 25 | ] 26 | 27 | DJANGO_DEBUG_TOOLBAR = True 28 | SHOW_TOOLBAR_CALLBACK = True 29 | 30 | COMPRESS_ENABLED = False 31 | COMPRESS_OFFLINE = True 32 | 33 | BROKER_URL = 'amqp://rabbit_user:rabbit_user_default_pass@localhost:5672/' 34 | -------------------------------------------------------------------------------- /project_name/project_name/general/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-boilerplate/35ed124d3290417037dcb382936eebe62dd2f13f/project_name/project_name/general/__init__.py -------------------------------------------------------------------------------- /project_name/project_name/general/redis_rabbitmq.py: -------------------------------------------------------------------------------- 1 | ''' 2 | The setup for rabbitmq message broker (we can also user Redis for this) 3 | ''' 4 | import os 5 | from kombu import Exchange, Queue, serialization 6 | import djcelery 7 | from {{ project_name }}.utils import make_memcached_cache 8 | 9 | 10 | # REDIS 11 | 12 | SESSION_CACHE_ALIAS = "sessions" 13 | SESSION_ENGINE = "django.contrib.sessions.backends.cache" 14 | 15 | REDIS_HOSTNAME = 'localhost' 16 | REDIS_PORT = 6379 17 | REDIS_SERVER = (REDIS_HOSTNAME, REDIS_PORT, 0) # host, port, db 18 | REDIS_PASSWORD = '' 19 | REDIS_DB = 1 # keep cache entries in a different db so we can clear them easily 20 | REDIS_HOST = os.environ.get('REDIS_PORT_6379_TCP_ADDR', 'redis') 21 | 22 | 23 | CACHES = { 24 | "default": { 25 | 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', 26 | }, 27 | "sessions": { 28 | "BACKEND": "redis_cache.cache.RedisCache", 29 | "LOCATION": 'redis:6379', 30 | "OPTIONS": { 31 | 'DB': 10, 32 | }, 33 | "KEY_PREFIX": "sessions" 34 | }, 35 | 'staticfiles': { 36 | 'BACKEND': 'redis_cache.cache.RedisCache', 37 | 'LOCATION': 'redis:6379', 38 | 'TIMEOUT': 86400 * 365, 39 | 'KEY_PREFIX': 'staticfiles', 40 | }, 41 | 'api': { 42 | 'BACKEND': 'redis_cache.cache.RedisCache', 43 | 'LOCATION': 'redis:6379', 44 | 'TIMEOUT': 86400 * 365, 45 | 'KEY_PREFIX': 'api', 46 | }, 47 | } 48 | 49 | 50 | # RABBITMQ 51 | RABBIT_HOSTNAME = os.environ.get('RABBIT_PORT_5672_TCP', 'rabbit') 52 | 53 | if RABBIT_HOSTNAME.startswith('tcp://'): 54 | RABBIT_HOSTNAME = RABBIT_HOSTNAME.split('//')[1] 55 | 56 | BROKER_URL = os.environ.get('BROKER_URL', '') 57 | if not BROKER_URL: 58 | BROKER_URL = 'amqp://{user}:{password}@{hostname}/{vhost}/'.format( 59 | user=os.environ.get('RABBIT_ENV_USER', 'rabbit_user'), 60 | password=os.environ.get('RABBIT_ENV_RABBITMQ_PASS', 'rabbit_user_default_pass'), 61 | hostname=RABBIT_HOSTNAME, 62 | vhost=os.environ.get('RABBIT_ENV_VHOST', '')) 63 | 64 | 65 | # We don't want to have dead connections stored on rabbitmq, so we have to negotiate using heartbeats 66 | BROKER_HEARTBEAT = '?heartbeat=30' 67 | if not BROKER_URL.endswith(BROKER_HEARTBEAT): 68 | BROKER_URL += BROKER_HEARTBEAT 69 | 70 | BROKER_POOL_LIMIT = 1 71 | BROKER_CONNECTION_TIMEOUT = 10 72 | 73 | 74 | # CELERY CONFIGURATION 75 | # CONFIGURE QUEUES, CURRENTLY WE HAVE ONLY ONE 76 | CELERY_CREATE_MISSING_QUEUES = True 77 | CELERY_RESULT_PERSISTENT = True 78 | 79 | CELERY_DEFAULT_QUEUE = 'default' 80 | CELERY_QUEUES = ( 81 | Queue('default', Exchange('default'), routing_key='default'), 82 | Queue('{{ project_name }}', Exchange('{{ project_name }}'), routing_key='{{ project_name }}'), 83 | ) 84 | 85 | 86 | # SENSIBLE SETTINGS FOR CELERY 87 | CELERY_ALWAYS_EAGER = False 88 | CELERY_ACKS_LATE = True 89 | CELERY_TASK_PUBLISH_RETRY = True 90 | CELERY_DISABLE_RATE_LIMITS = False 91 | 92 | # BY DEFAULT WE WILL IGNORE RESULT 93 | # IF YOU WANT TO SEE RESULTS AND TRY OUT TASKS INTERACTIVELY, CHANGE IT TO FALSE 94 | # OR CHANGE THIS SETTING ON TASKS LEVEL 95 | CELERY_IGNORE_RESULT = True 96 | CELERY_SEND_TASK_ERROR_EMAILS = False 97 | CELERY_TASK_RESULT_EXPIRES = 600 98 | 99 | # SET REDIS AS CELERY RESULT BACKEND 100 | CELERY_RESULT_BACKEND = 'redis://%s:%d/%d' % (REDIS_HOSTNAME, REDIS_PORT, REDIS_DB) 101 | CELERY_REDIS_MAX_CONNECTIONS = 1 102 | 103 | # DON'T USE PICKLE AS SERIALIZER, JSON IS MUCH SAFER 104 | CELERY_ACCEPT_CONTENT = ['json'] 105 | CELERY_TASK_SERIALIZER = 'json' 106 | CELERY_RESULT_SERIALIZER = 'json' 107 | 108 | CELERYD_HIJACK_ROOT_LOGGER = False 109 | CELERYD_PREFETCH_MULTIPLIER = 1 110 | CELERYD_MAX_TASKS_PER_CHILD = 1000 111 | 112 | 113 | # ACTIVATE SETTINGS 114 | djcelery.setup_loader() 115 | -------------------------------------------------------------------------------- /project_name/project_name/general/settings.py: -------------------------------------------------------------------------------- 1 | from django.utils.translation import ugettext_lazy 2 | 3 | LOGIN_REDIRECT_URL = '/' 4 | LOGOUT_REDIRECT_URL = '/' 5 | LOGIN_URL = '/login/' 6 | 7 | CANONICAL_SITE_URL = 'www.{{ project_name }}.com' 8 | 9 | AUTH_PROFILE_MODULE = 'profiles.profile' 10 | ABSOLUTE_URL_OVERRIDES = { 11 | 'auth.user': lambda u: '/user/%i/' % u.profile.pk, 12 | } 13 | 14 | # Sessions 15 | POSTGRES_HOSTNAME = 'localhost' 16 | 17 | # LANGUAGES 18 | LANGUAGE_CODE = 'en-gb' 19 | 20 | LANGUAGES = ( 21 | ('en-us', ugettext_lazy('American English')), 22 | ('en-gb', ugettext_lazy('British English')), 23 | ) 24 | 25 | # Pseudolanguage is used for Enabling Crowdin translation mode 26 | PSEUDO_LANGUAGE = 'zu' 27 | VISIBLE_LANGUAGES = tuple(x for x in LANGUAGES if x[0] != PSEUDO_LANGUAGE) 28 | 29 | LOGGING = { 30 | "version": 1, 31 | "disable_existing_loggers": False, 32 | 'formatters': { 33 | 'json': { 34 | '()': 'pythonjsonlogger.jsonlogger.JsonFormatter', 35 | 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' 36 | }, 37 | 'verbose': { 38 | 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' 39 | }, 40 | }, 41 | "handlers": { 42 | 'bugsnag': { 43 | 'level': 'ERROR', 44 | 'class': 'bugsnag.handlers.BugsnagHandler', 45 | }, 46 | 'console': { 47 | 'level': 'INFO', 48 | 'class': 'logging.StreamHandler', 49 | 'formatter': 'json', 50 | }, 51 | 'investigation': { 52 | "level": "INFO", 53 | "class": "bugsnag.handlers.BugsnagHandler" 54 | } 55 | }, 56 | "loggers": { 57 | "django.request": { 58 | "level": "ERROR", 59 | "propagate": True, 60 | }, 61 | "celery": { 62 | "handlers": ["console", "bugsnag"], 63 | "level": "INFO", 64 | "propagate": True 65 | }, 66 | "httpstream": { 67 | "handlers": ["console", "bugsnag"], 68 | "level": "ERROR", 69 | "propagate": True 70 | }, 71 | "elasticsearch": { 72 | "handlers": ["console", "bugsnag"], 73 | "level": "WARNING", 74 | "propagate": False 75 | }, 76 | "urllib3": { 77 | "handlers": ["console", "bugsnag"], 78 | "level": "WARNING", 79 | "propagate": False 80 | }, 81 | "celery.investigation": { 82 | "level": "INFO", 83 | "handlers": ["investigation"], 84 | "propagate": False 85 | } 86 | 87 | } 88 | } 89 | 90 | -------------------------------------------------------------------------------- /project_name/project_name/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from {{ project_name }}.databases import * 4 | from {{ project_name }}.general.settings import * 5 | from {{ project_name }}.third_parties.blacklist_domains import * 6 | from {{ project_name }}.general.redis_rabbitmq import * 7 | 8 | SITE_ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 9 | {{ project_name }}_VERSION = os.path.basename(os.path.dirname(SITE_ROOT)) 10 | DJANGOENV = None 11 | 12 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 13 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 14 | 15 | 16 | # Quick-start development settings - unsuitable for production 17 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 18 | 19 | # IMPORTANT!: 20 | # You must keep this secret, you can store it in an 21 | # environment variable and set it with: 22 | # export SECRET_KEY="secret-export!key-781" 23 | # https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/#secret-key 24 | SECRET_KEY = "secret_change_this_after" 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = False 28 | 29 | ALLOWED_HOSTS = ['*'] 30 | 31 | # Application definition 32 | SITE_ID = 1 33 | 34 | BASE_INSTALLED_APPS = [ 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 'django.contrib.sites', 42 | 'django.contrib.humanize', 43 | 'django.contrib.sitemaps', 44 | 'django.contrib.redirects', 45 | 'djcelery', 46 | 'lib.common', 47 | 'lib.audit', 48 | 'lib.cache', 49 | 'lib.viewslibrary', 50 | 'statici18n', 51 | 'compressor', 52 | 'rest_framework', 53 | 'channels', 54 | ] 55 | 56 | INSTALLED_APPS = [ 57 | 'accounts', 58 | 'api', 59 | ] + BASE_INSTALLED_APPS 60 | 61 | 62 | MIDDLEWARE = [ 63 | 'django.middleware.security.SecurityMiddleware', 64 | 'django.contrib.sessions.middleware.SessionMiddleware', 65 | 'django.middleware.common.CommonMiddleware', 66 | 'django.middleware.csrf.CsrfViewMiddleware', 67 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 68 | 'django.contrib.messages.middleware.MessageMiddleware', 69 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 70 | ] 71 | 72 | REST_FRAMEWORK = { 73 | # Use Django's standard `django.contrib.auth` permissions, 74 | # or allow read-only access for unauthenticated users. 75 | 'DEFAULT_PERMISSION_CLASSES': [ 76 | 'rest_framework.authentication.BasicAuthentication', 77 | 'rest_framework.authentication.SessionAuthentication', 78 | 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly', 79 | 80 | ], 81 | 'DEFAULT_RENDERER_CLASSES': ( 82 | 'rest_framework.renderers.JSONRenderer', 83 | ), 84 | # 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 85 | # 'PAGE_SIZE': 20 86 | } 87 | 88 | 89 | ROOT_URLCONF = '{{ project_name }}.urls' 90 | 91 | TEMPLATES = [ 92 | { 93 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 94 | 'DIRS': [ 95 | os.path.join(SITE_ROOT, 'templates'), 96 | os.path.join(SITE_ROOT, 'static'), 97 | ], 98 | 'APP_DIRS': False, 99 | 'OPTIONS': { 100 | 'context_processors': [ 101 | 'django.template.context_processors.debug', 102 | 'django.template.context_processors.request', 103 | 'django.contrib.auth.context_processors.auth', 104 | 'django.contrib.messages.context_processors.messages', 105 | 'django.template.context_processors.i18n', 106 | 'django.template.context_processors.csrf', 107 | 108 | ], 109 | 'loaders': [ 110 | 'django.template.loaders.filesystem.Loader', 111 | 'django.template.loaders.app_directories.Loader', 112 | 'django.template.loaders.eggs.Loader', 113 | ], 114 | 'debug': DEBUG, 115 | }, 116 | }, 117 | ] 118 | 119 | # Static files (CSS, JavaScript, Images) 120 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 121 | 122 | STATIC_URL = '/static/' 123 | 124 | STATICFILES_DIRS = [] 125 | 126 | STATICFILES_STORAGE = 'lib.storage.CachedStaticFilesStorage' 127 | 128 | STATICFILES_FINDERS = ( 129 | 'django.contrib.staticfiles.finders.FileSystemFinder', 130 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 131 | 'compressor.finders.CompressorFinder', 132 | ) 133 | 134 | # Statici18n Config 135 | STATICI18N_ROOT = os.path.join(SITE_ROOT, 'static') 136 | 137 | STATICFILES_DIRS += [STATICI18N_ROOT] 138 | 139 | STATIC_ROOT = os.path.join(SITE_ROOT, 'staticfiles') 140 | 141 | LOCALE_PATHS = [os.path.join(SITE_ROOT, 'locale')] 142 | 143 | COMPRESS_ROOT = '/tmp/pr-static' 144 | COMPRESS_ENABLED = True 145 | 146 | WSGI_APPLICATION = '{{ project_name }}.wsgi.application' 147 | 148 | # Password validation 149 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 150 | 151 | AUTH_PASSWORD_VALIDATORS = [ 152 | { 153 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 154 | }, 155 | { 156 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 157 | }, 158 | { 159 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 160 | }, 161 | { 162 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 163 | }, 164 | ] 165 | 166 | AUTH_USER_MODEL = 'accounts.User' 167 | 168 | # Internationalization 169 | # https://docs.djangoproject.com/en/1.11.4/topics/i18n/ 170 | 171 | LANGUAGE_CODE = 'en-us' 172 | 173 | INTERNAL_IPS = ('127.0.0.1', '192.168.56.1', '10.0.2.2',) 174 | 175 | TIME_ZONE = 'UTC' 176 | 177 | USE_I18N = True 178 | 179 | USE_L10N = True 180 | 181 | USE_TZ = True 182 | 183 | 184 | # Pull slug max_length out ot 185 | SLUG_MAX_LENGTH = 64 186 | 187 | LOGIN_REDIRECT_URL = '/' 188 | LOGOUT_REDIRECT_URL = '/' 189 | -------------------------------------------------------------------------------- /project_name/project_name/staging/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-boilerplate/35ed124d3290417037dcb382936eebe62dd2f13f/project_name/project_name/staging/__init__.py -------------------------------------------------------------------------------- /project_name/project_name/staging/settings.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-boilerplate/35ed124d3290417037dcb382936eebe62dd2f13f/project_name/project_name/staging/settings.py -------------------------------------------------------------------------------- /project_name/project_name/testing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-boilerplate/35ed124d3290417037dcb382936eebe62dd2f13f/project_name/project_name/testing/__init__.py -------------------------------------------------------------------------------- /project_name/project_name/testing/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | testing.settings will pull in (probably global) local_settings, 3 | This is a special thanks to David Dyball for helping me understand and build something very familiar to me 4 | in terms of settings and how to set them up 5 | 6 | To all who contribute for this, thank you very much. 7 | """ 8 | from {{ project_name }}.settings import * 9 | import os 10 | 11 | ''' 12 | If you are using windows by default, the permissions to access subfolders for tests are disabled 13 | Activate them using NOSE_INCLUDE_EXE = 1 or an environment variable in your OS with the same name and value 14 | ''' 15 | NOSE_INCLUDE_EXE = 1 16 | ''' 17 | Other settings 18 | ''' 19 | DEBUG = True 20 | TESTING = True 21 | 22 | ''' 23 | Tells the django environment 24 | ''' 25 | DJANGOENV = "testing" 26 | 27 | REUSE_DB = bool(int(os.environ.get("REUSE_DB", 0))) 28 | 29 | DATABASES = { 30 | 'default': { 31 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 32 | 'NAME': 'postgres', 33 | 'USER': 'postgres', 34 | 'PASSWORD': 'postgres', 35 | 'HOST': 'postgres', 36 | 'PORT': '5432', 37 | } 38 | } 39 | 40 | if REUSE_DB: 41 | DATABASE_ROUTERS = [] 42 | 43 | POSTGIS_TEMPLATE = "template_postgis" 44 | POSTGIS_VERSION = (2, 1, 2) 45 | 46 | SOUTH_TESTS_MIGRATE = bool(int(os.environ.get("MIGRATE", 0))) 47 | 48 | # If you have Django Debug Toolbar installed 49 | DEBUG_TOOLBAR_PANELS = () 50 | 51 | # Disable the Secure SSL Redirect (special thanks to @DD) 52 | SECURE_SSL_REDIRECT = False 53 | 54 | # Use this if you have local_settings.pt file 55 | DEFAULT_FILE_STORAGE = "django.core.files.storage.FileSystemStorage" 56 | MEDIA_URL = '/media/' 57 | STATIC_ROOT = '/tmp/assets-upload' 58 | STATIC_URL = "/static/" 59 | MEDIA_ROOT = '/tmp/media-root' 60 | 61 | # Roll in the API URLs so we can test everything in one go 62 | API_URLS_IN_MAIN = True 63 | 64 | # nosetests - NoseTestSuiteRunner 65 | TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' 66 | 67 | # Args passed to nose 68 | NOSE_ARGS = ["--logging-clear-handlers", "--logging-filter", "INFO"] 69 | 70 | # Give ourselves a test instance of redis (another special thanks to @DD for this) 71 | REDIS_SERVER = ('redis', 6379, 2) # host, port, dbs 72 | REDIS_PASSWORD = None 73 | 74 | # We don't want to run Memcached for tests. 75 | SESSION_ENGINE = "django.contrib.sessions.backends.db" 76 | 77 | 78 | CACHES = { 79 | 'default': { 80 | 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', 81 | }, 82 | 'api': { 83 | 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', 84 | }, 85 | 'staticfiles': { 86 | 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', 87 | }, 88 | 'depictions': { 89 | 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', 90 | }, 91 | 'thumbnails': { 92 | 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', 93 | }, 94 | } 95 | 96 | # allow for a local override that won't be used by development 97 | try: 98 | from {{ project_name }}.{{ project_name }}.testing.local_settings import * 99 | except ImportError: 100 | pass 101 | 102 | 103 | # We don't care about secure password for tests, use MD5 which is faster. 104 | PASSWORD_HASHERS = ( 105 | 'django.contrib.auth.hashers.MD5PasswordHasher', 106 | # This one is only needed for some of the old fixtures. 107 | 'django.contrib.auth.hashers.SHA1PasswordHasher', 108 | ) 109 | 110 | 111 | LOGGING = { 112 | "version": 1, 113 | "disable_existing_loggers": True, 114 | 'root': { 115 | 'level': 'INFO', 116 | 'handlers': ['console'], 117 | }, 118 | 'formatters': { 119 | 'verbose': { 120 | 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' 121 | }, 122 | }, 123 | "handlers": { 124 | 'console': { 125 | 'level': 'DEBUG', 126 | 'class': 'logging.StreamHandler', 127 | 'formatter': 'verbose' 128 | }, 129 | } 130 | } 131 | 132 | 133 | MIDDLEWARE = list(MIDDLEWARE) 134 | if 'bugsnag.django.middleware.BugsnagMiddleware' in MIDDLEWARE: 135 | MIDDLEWARE.remove('bugsnag.django.middleware.BugsnagMiddleware') 136 | 137 | COMPRESS_ENABLED = False 138 | -------------------------------------------------------------------------------- /project_name/project_name/third_parties/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsil/django-boilerplate/35ed124d3290417037dcb382936eebe62dd2f13f/project_name/project_name/third_parties/__init__.py -------------------------------------------------------------------------------- /project_name/project_name/third_parties/blacklist_domains.py: -------------------------------------------------------------------------------- 1 | BLACKLISTED_DOMAINS = { 2 | "2rainmail.org.uk", 3 | "armyspy.com", 4 | "barchor.org.uk", 5 | "cannotmail.org.uk", 6 | "cool.fr.nf", 7 | "courriel.fr.nf", 8 | "crymet.org.uk", 9 | "cuvox.de", 10 | "dayrep.com", 11 | "drecom01.co.uk", 12 | "eelmail.com", 13 | "einrot.com", 14 | "example.com", 15 | "europe.eu", 16 | "europe.com", 17 | "fleckens.hu", 18 | "freggnet.co.uk", 19 | "gamil.com", 20 | "gmail.con", 21 | "gmail.co.uk", 22 | "gmail.co", 23 | "gmil.com", 24 | "gmail.pt", 25 | "gmil.pt", 26 | "grr.la", 27 | "guerrillamail.biz", 28 | "guerrillamail.com", 29 | "guerrillamail.de", 30 | "guerrillamail.net", 31 | "guerrillamail.org", 32 | "guerrillamailblock.com", 33 | "gustr.com", 34 | "homail.co.uk", 35 | "homail.com", 36 | "hotmail.co", 37 | "hoodmail.co.uk", 38 | "jetable.fr.nf", 39 | "jourrapide.com", 40 | "kreahnet.org.uk", 41 | "lonynet.org.uk", 42 | "mail.cat", 43 | "mail.fr", 44 | "mailbreaker.co.uk", 45 | "mailinator.com", 46 | "mega.zik.dj", 47 | "moncourrier.fr.nf", 48 | "monemail.fr.nf", 49 | "monmail.fr.nf", 50 | "moussenetmail.co.uk", 51 | "mywheelboxmail.org.uk", 52 | "nomail.xl.cx", 53 | "nospam.ze.tc", 54 | "pluntermail.org.uk", 55 | "prainnet.org.uk", 56 | "rackernet.org.uk", 57 | "railosnet.co.uk", 58 | "rhyta.com", 59 | "rottmail.co.uk", 60 | "runracemail.org.uk", 61 | "runwaynet.org.uk", 62 | "sharklasers.com", 63 | "sherrymail.co.uk", 64 | "shortsmail.co.uk", 65 | "spam.co.uk", 66 | "spam.com", 67 | "spam4.me", 68 | "speed.1s.fr", 69 | "stonetimenet.co.uk", 70 | "superrito.com", 71 | "teleworm.us", 72 | "telph1line.org.uk", 73 | "test.be", 74 | "test.co", 75 | "test.com", 76 | "test.de", 77 | "test.eu", 78 | "test.it", 79 | "test.fr", 80 | "test.nl", 81 | "test.net", 82 | "test.pl", 83 | "threemailnet.co.uk", 84 | "trbvm.com", 85 | "tyermail.org.uk", 86 | "wonandron.co.uk", 87 | "wormail.co.uk", 88 | "ya.com", 89 | "yopmail.com", 90 | "yopmail.fr", 91 | "yopmail.net", 92 | } -------------------------------------------------------------------------------- /project_name/project_name/urls.py: -------------------------------------------------------------------------------- 1 | """{{ project_name }} URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url, include 17 | from django.contrib import admin 18 | from accounts.views import LoginView, LogoutView, HomepageView 19 | import accounts.urls 20 | import api.urls 21 | 22 | admin.autodiscover() 23 | 24 | urlpatterns = [ 25 | url(r'^admin/', admin.site.urls), 26 | url(r'^$', HomepageView.as_view(), name=('homepage')), 27 | url(r'^login/$', LoginView.as_view(), name=('login')), 28 | url(r'^logout/$', LogoutView.as_view(), name=('logout')), 29 | url(r'^u/', include(accounts.urls.accounts_urlpatterns, namespace="accounts")), 30 | 31 | # API 32 | url(r'^api/v1/', include(api.urls.api_url_pattern, namespace="api-v1")), 33 | ] 34 | -------------------------------------------------------------------------------- /project_name/project_name/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for {{ project_name }} project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /project_name/static/css/project_name.css: -------------------------------------------------------------------------------- 1 | .log-in-form { 2 | border: 1px solid $medium-gray; 3 | padding: $form-spacing; 4 | border-radius: $global-radius; 5 | } -------------------------------------------------------------------------------- /project_name/templates/project_name/application/core.html: -------------------------------------------------------------------------------- 1 | {% templatetag openblock %}load static from staticfiles {% templatetag closeblock %} 2 | {% templatetag openblock %}load statici18n {% templatetag closeblock %} 3 | {% templatetag openblock %}load i18n {% templatetag closeblock %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% templatetag openblock %} block page-title {% templatetag closeblock %} Login | {{ project_name }} {% templatetag openblock %} endblock {% templatetag closeblock %} 11 | {% templatetag openblock %} block head-css {% templatetag closeblock %} 12 | 13 | 14 | {% templatetag openblock %} endblock {% templatetag closeblock %} 15 | {% templatetag openblock %} block secondary-css {% templatetag closeblock %}{% templatetag openblock %} endblock {% templatetag closeblock %} 16 | 17 | 18 | 19 | {% templatetag openblock %} block head-messages {% templatetag closeblock %} 20 |
21 | {% templatetag openblock %} include "{{ project_name }}/shared/partials/messages.html" %} 22 |
23 | {% templatetag openblock %} endblock {% templatetag closeblock %} 24 | 25 |
26 |
27 |
28 | {% templatetag openblock %} block content {% templatetag closeblock %}{% templatetag openblock %} endblock {% templatetag closeblock %} 29 |
30 |
31 |
32 | 33 | 41 | 42 | {% templatetag openblock %} block bottom-js {% templatetag closeblock %} 43 | 44 | 45 | {% templatetag openblock %} endblock {% templatetag closeblock %} 46 | {% templatetag openblock %} block secondary-js {% templatetag closeblock %}{% templatetag openblock %} endblock {% templatetag closeblock %} 47 | 48 | 49 | -------------------------------------------------------------------------------- /project_name/templates/project_name/application/homepage/homepage.html: -------------------------------------------------------------------------------- 1 | {% templatetag openblock %} extends '{{ project_name }}/application/core.html' {% templatetag closeblock %} 2 | 3 | {% templatetag openblock %} block content {% templatetag closeblock %} 4 |

Page is working

5 |
Welcome {% templatetag openvariable %} request.user.display_name {% templatetag closevariable %}
6 | {% templatetag openblock %} endblock {% templatetag closeblock %} -------------------------------------------------------------------------------- /project_name/templates/project_name/auth/core.html: -------------------------------------------------------------------------------- 1 | {% templatetag openblock %}load static from staticfiles {% templatetag closeblock %} 2 | {% templatetag openblock %}load statici18n {% templatetag closeblock %} 3 | {% templatetag openblock %}load i18n {% templatetag closeblock %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% templatetag openblock %} block page-title {% templatetag closeblock %} Login | {{ project_name }} {% templatetag openblock %} endblock {% templatetag closeblock %} 11 | {% templatetag openblock %} block head-css {% templatetag closeblock %} 12 | 13 | 14 | {% templatetag openblock %} endblock {% templatetag closeblock %} 15 | {% templatetag openblock %} block secondary-css {% templatetag closeblock %}{% templatetag openblock %} endblock {% templatetag closeblock %} 16 | 17 | 18 | 19 | {% templatetag openblock %} block head-messages {% templatetag closeblock %} 20 |
21 | {% templatetag openblock %} include "{{ project_name }}/shared/partials/messages.html" %} 22 |
23 | {% templatetag openblock %} endblock {% templatetag closeblock %} 24 | 25 |
26 |
27 |
28 | {% templatetag openblock %} block content {% templatetag closeblock %}{% templatetag openblock %} endblock {% templatetag closeblock %} 29 |
30 |
31 |
32 | 33 | 40 | 41 | {% templatetag openblock %} block bottom-js {% templatetag closeblock %} 42 | 43 | 44 | {% templatetag openblock %} endblock {% templatetag closeblock %} 45 | {% templatetag openblock %} block secondary-js {% templatetag closeblock %}{% templatetag openblock %}endblock {% templatetag closeblock %} 46 | 47 | -------------------------------------------------------------------------------- /project_name/templates/project_name/auth/login.html: -------------------------------------------------------------------------------- 1 | {% templatetag openblock %} extends '{{ project_name }}/auth/core.html' {% templatetag closeblock %} 2 | 3 | {% templatetag openblock %} block content {% templatetag closeblock %} 4 | 15 | {% templatetag openblock %} endblock {% templatetag closeblock %} 16 | -------------------------------------------------------------------------------- /project_name/templates/project_name/shared/forms/foundation_field.html: -------------------------------------------------------------------------------- 1 |
2 | {% templatetag openblock %} if label or field.label {% templatetag closeblock %} 3 | {% templatetag openblock %} if not hide_label {% templatetag closeblock %} 4 | 7 | {% templatetag openblock %} endif {% templatetag closeblock %} 8 | {% templatetag openblock %} endif {% templatetag closeblock %} 9 | {% templatetag openvariable %} field {% templatetag closevariable %} 10 | {% templatetag openblock %} if field.errors {% templatetag closeblock %}{% templatetag openvariable %} field.errors {% templatetag closevariable %}{% templatetag openblock %} endif {% templatetag closeblock %} 11 |
12 | -------------------------------------------------------------------------------- /project_name/templates/project_name/shared/forms/foundation_field_basic.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {% templatetag openvariable %} field.label {% templatetag closevariable %}{% templatetag openblock %} if not hide_asterisks {% templatetag closeblock %}*{% templatetag openblock %} endif {% templatetag closeblock %} 4 |
5 |
6 | {% templatetag openvariable %} field {% templatetag closevariable %} 7 |
8 | 9 | {% templatetag openvariable %} field.errors {% templatetag closevariable %} 10 | 11 |
-------------------------------------------------------------------------------- /project_name/templates/project_name/shared/partials/messages.html: -------------------------------------------------------------------------------- 1 | {% templatetag openblock %} if messages {% templatetag closeblock %} 2 | {% templatetag openblock %} for message in messages {% templatetag closeblock %} 3 | {% templatetag openblock %} if message.tags == "success" {% templatetag closeblock %}
{% templatetag openblock %} endif {% templatetag closeblock %} 4 | {% templatetag openblock %} if message.tags == "error" {% templatetag closeblock %}
{% templatetag openblock %} endif {% templatetag closeblock %} 5 | {% templatetag openblock %} if message.tags == "info" {% templatetag closeblock %}
{% templatetag openblock %} endif {% templatetag closeblock %} 6 | {% templatetag openblock %} if message.tags == "warning" {% templatetag closeblock %}
{% templatetag openblock %} endif {% templatetag closeblock %} 7 | {% templatetag openvariable %} message {% templatetag closevariable %} 8 | 12 |
13 | {% templatetag openblock %} endfor {% templatetag closeblock %} 14 | {% templatetag openblock %} endif {% templatetag closeblock %} 15 | -------------------------------------------------------------------------------- /requirements/common.txt: -------------------------------------------------------------------------------- 1 | anyjson==0.3.3 2 | astroid==1.6.1 3 | awesome-slugify==1.6.5 4 | beautifulsoup4==4.8.1 5 | billiard==3.5.0.3 6 | bitly-api==0.3 7 | bleach==3.1.4 8 | celery==4.3.0 9 | channels==2.3.1 10 | cffi==1.11.5 11 | cryptography==2.8 12 | cssselect==1.0.3 13 | django_exceptional_middleware==1.2.1 14 | djangorestframework==3.11.0 15 | djangorestframework-simplejwt==4.4.0 16 | django-rest-swagger==2.2.0 17 | django-anymail[mailgun]==2.2 18 | django-annoying==0.10.4 19 | django-appconf==1.0.2 20 | django-block-ip==0.2.0 21 | django-celery==3.2.2 22 | django-cors-headers==2.4.0 23 | django-countries==5.5 24 | django-elasticsearch-dsl==0.5.1 25 | django-elasticsearch-dsl-drf==0.17.6 26 | django-geoposition==0.3.0 27 | django-filter==2.0.0 28 | django-import-export==1.2.0 29 | django-leaflet==0.24.0 30 | django-localflavor==2.1 31 | django-location-field==2.0.3 32 | django-mail-factory==0.20 33 | django-markup-deprecated==0.0.3 34 | django-memoize==2.1.1 35 | django-modeltranslation==0.12.2 36 | django-money==1.0 37 | django-phonenumber-field==4.0.0 38 | django-redis==4.11.0 39 | django-secure==1.0.1 40 | django-sql-explorer==1.1.2 41 | django-statici18n==1.8.2 42 | django-storages==1.7.1 43 | django-transaction-hooks==0.2 44 | django-validate-model-attribute-assignment==2.0.2 45 | Django==2.2.11 46 | dnspython==1.15.0 47 | elasticsearch==6.3.1 48 | elasticsearch-dsl==6.1.0 49 | enum34==1.1.6 50 | fuzzywuzzy==0.17.0 51 | geopy==1.17.0 52 | geoip2==2.9.0 53 | lazy-object-proxy==1.3.1 54 | hashids==1.2.0 55 | httplib2==0.18.0 56 | isort==4.3.4 57 | Jinja2==2.10.3 58 | jsonfield==2.0.2 59 | lockfile==0.12.2 60 | Markdown==2.6.11 61 | markdown2==2.3.8 62 | netaddr==0.7.19 63 | phonenumbers==8.11.2 64 | phone_iso3166==0.3.5 65 | Pillow==6.2.0 66 | postgres==3.0.0 67 | premailer==3.2.0 68 | progressbar2==3.47.0 69 | pyaml==19.4.1 70 | pycountry==18.5.26 71 | python-dateutil==2.6.1 72 | python-Levenshtein==0.12.0 73 | python-json-logger==0.1.9 74 | python-magic==0.4.15 75 | pyOpenSSL==18.0.0 76 | pytz==2018.5 77 | rabbitmq==0.2.0 78 | requests==2.22.0 79 | redis==2.10.6 80 | regex==2019.8.19 81 | restless==2.1.1 82 | simplejson==3.16.0 83 | six==1.11.0 84 | tldextract==2.2.0 85 | unicodecsv==0.14.1 86 | urbanairship==4.0.1 87 | urllib3==1.25.7 88 | user-agents==1.1.0 89 | uWSGI==2.0.18 90 | Wand==0.4.4 91 | wrapt==1.10.11 92 | -------------------------------------------------------------------------------- /requirements/deployment.txt: -------------------------------------------------------------------------------- 1 | -r common.txt 2 | fabric==1.14.0 3 | paramiko==1.7.7.1 4 | -------------------------------------------------------------------------------- /requirements/development.txt: -------------------------------------------------------------------------------- 1 | -r common.txt 2 | coverage==4.5.1 3 | dj-database-url==0.5.0 4 | django-devserver==0.8.0 5 | django-extensions==2.0.3 6 | django-i18n-helper==0.1.1 7 | django-nose==1.4.5 8 | django-template-repl==0.3.0 9 | django-webtest==1.9.2 10 | factory_boy==2.10.0 11 | freezegun==0.3.10 12 | ipdb==0.11 13 | mock==2.0.0 14 | nose==1.3.7 15 | nosexcover==1.0.11 16 | path.py==11 17 | pinocchio==0.4.2 18 | pre-commit==1.7.0 19 | pudb==2017.1.4 20 | PyVirtualDisplay==0.2.1 21 | PyYAML==5.1 22 | sphinx==1.7.1 23 | sqlparse==0.2.4 24 | spec==1.4.1 25 | transifex-client==0.13.1 26 | unittest-data-provider==1.0.1 27 | virtualenv-tools==1.0 28 | webtest==2.0.29 29 | werkzeug==0.15.3 30 | -------------------------------------------------------------------------------- /requirements/live.txt: -------------------------------------------------------------------------------- 1 | -r common.txt 2 | -------------------------------------------------------------------------------- /requirements/windows/common.txt: -------------------------------------------------------------------------------- 1 | aldjemy==0.8.0 2 | anyjson==0.3.3 3 | astroid==1.6.1 4 | awesome-slugify==1.6.5 5 | awscli==1.14.53 6 | beautifulsoup4==4.6.0 7 | billiard==3.5.0.3 8 | bitly-api==0.3 9 | bleach==3.1.4 10 | boto==2.48.0 11 | botocore==1.9.6 12 | boto3==1.6.6 13 | braintree==3.42.0 14 | bugsnag==3.4.2 15 | business-calendar==0.2.1 16 | celery==3.1.25 17 | cffi==1.11.5 18 | channels==2.0.2 19 | colorama==0.3.9 20 | cryptography==2.1.4 21 | cssselect==1.0.3 22 | django_exceptional_middleware==1.2.1 23 | djangorestframework==3.9.1 24 | django-appconf==1.0.2 25 | django-classy-tags==0.8.0 26 | django-annoying==0.10.4 27 | django-block-ip==0.2.0 28 | django-celery==3.2.2 29 | django-channels==0.7.0 30 | django-countries==5.2 31 | django-compressor==2.2 32 | django-crispy-forms==1.7.1 33 | django-debug-toolbar==1.9.1 34 | django-easy-maps==0.9.3 35 | django-grappelli==2.11.1 36 | django-geoposition==0.3.0 37 | django-iban==0.3.1 38 | django-libsass==0.7 39 | django-leaflet==0.23.0 40 | django-localflavor==2.0 41 | django-mail-factory==0.20 42 | django-location-field==2.0.3 43 | django-markup-deprecated==0.0.3 44 | django-memoize==2.1.0 45 | django-mmc==1.6.5 46 | django-modeltranslation==0.12.2 47 | django-pandas==0.5.1 48 | django-postman==3.6.1 49 | django-redis-cache==1.7.1 50 | django-secure==1.0.1 51 | django-sql-explorer==1.1.1 52 | django-statici18n==1.7.0 53 | django-storages==1.6.5 54 | django-taggit==0.22.2 55 | django-transaction-hooks==0.2 56 | django-treebeard==4.2.1 57 | django-validate-model-attribute-assignment==2.0.2 58 | Django==1.11.25 59 | djrill==2.1.0 60 | elasticsearch==6.1.1 61 | elasticsearch-dsl==6.1.0 62 | fuzzywuzzy==0.16.0 63 | geopy==1.11.0 64 | gdata==2.0.18 65 | lazy-object-proxy==1.3.1 66 | hashids==1.2.0 67 | invoke==0.22.1 68 | ipython==6.2.1 69 | IPy==0.83 70 | isort==4.3.4 71 | Jinja2==2.10 72 | jsonfield==2.0.2 73 | libsass==0.14.0 74 | linkedin==0.1.5 75 | lockfile==0.12.2 76 | mandrill==1.0.57 77 | Markdown==2.6.11 78 | markdown2==2.3.5 79 | mccabe==0.6.1 80 | newrelic==2.106.1.88 81 | numpy==1.14.1 82 | pandas==0.22.0 83 | pep8==1.7.1 84 | phonenumbers==8.9.1 85 | Pillow==5.0.0 86 | premailer==3.1.1 87 | progressbar2==3.36.0 88 | psycopg2==2.7.4 89 | pyaml==17.12.1 90 | pycountry==18.2.23 91 | python-dateutil==2.6.1 92 | python-Levenshtein==0.12.0 93 | python-json-logger==0.1.8 94 | python-magic==0.4.13 95 | python3-memcached==1.51 96 | python-linkedin==4.1 97 | pytz==2018.3 98 | rabbitmq==0.2.0 99 | raven==6.6.0 100 | requests==2.18.4 101 | redis==2.10.6 102 | reportlab==3.4.0 103 | restless==2.1.1 104 | simplejson==3.13.2 105 | six==1.11.0 106 | unicodecsv==0.14.1 107 | urbanairship==3.1.0 108 | urllib3==1.22 109 | user-agents==1.1.0 110 | vatnumber==1.2 111 | Wand==0.4.4 112 | xhtml2pdf==0.2.1 113 | -------------------------------------------------------------------------------- /requirements/windows/deployment.txt: -------------------------------------------------------------------------------- 1 | -r common.txt 2 | fabric==1.14.0 3 | paramiko==2.0.9 4 | 5 | -------------------------------------------------------------------------------- /requirements/windows/development.txt: -------------------------------------------------------------------------------- 1 | -r common.txt 2 | coverage==4.5.1 3 | dj-database-url==0.4.2 4 | django-devserver==0.8.0 5 | django-extensions==2.0.3 6 | django-i18n-helper==0.1.1 7 | django-nose==1.4.5 8 | django-template-repl==0.3.0 9 | django-webtest==1.9.2 10 | factory_boy==2.10.0 11 | freezegun==0.3.10 12 | ipdb==0.11 13 | mock==2.0.0 14 | nose==1.3.7 15 | nosexcover==1.0.11 16 | path.py==11 17 | pinocchio==0.4.2 18 | pre-commit==1.7.0 19 | pudb==2017.1.4 20 | PyVirtualDisplay==0.2.1 21 | PyYAML==5.1 22 | sphinx==1.7.1 23 | sqlparse==0.2.4 24 | spec==1.4.1 25 | transifex-client==0.13.1 26 | unittest-data-provider==1.0.1 27 | virtualenv-tools==1.0 28 | webtest==2.0.29 29 | werkzeug==0.15.3 30 | -------------------------------------------------------------------------------- /roles/tasks.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from jinja2 import Environment, FileSystemLoader 5 | from invoke import run, task 6 | 7 | 8 | def _template_file(template, destination, template_dir='/var/www/deploy/'): 9 | environment = Environment(loader=FileSystemLoader(template_dir)) 10 | template = environment.get_template(template) 11 | rendered_template = template.render(os.environ) 12 | if os.path.isfile(destination): 13 | os.unlink(destination) 14 | with open(destination, 'w') as f: 15 | f.write(rendered_template) 16 | 17 | 18 | @task 19 | def development(context): 20 | run('pip3 install -U pip') 21 | run('pip3 install -r requirements/development.txt') 22 | # _template_file('nginx/nginx.conf', '/etc/nginx/sites-enabled/default') 23 | # _template_file('supervisor.nginx.conf', '/etc/supervisor/conf.d/nginx.conf') 24 | # _template_file('supervisor.celery.conf', '/etc/supervisor/conf.d/celery.conf') 25 | run('supervisord -n') 26 | --------------------------------------------------------------------------------