├── .gitignore ├── .isort.cfg ├── .travis.yml ├── .vscode └── settings.json ├── LICENSE.txt ├── README.md ├── build-webapp.sh ├── buildspec.yml ├── contributors.txt ├── docker-compose.utest.yml ├── docker-compose.yml ├── logs └── README.md ├── nginx ├── Dockerfile └── sites-enabled │ └── django_project ├── requirements.txt ├── server ├── .coveragerc ├── .ebextensions │ ├── 01_main.config │ ├── 02_ec2.config │ ├── 03-loadbalancer.config │ ├── 04_notifications.config │ └── 05_logs.config ├── Dockerfile ├── apps │ ├── __init__.py │ ├── authentication │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── api_urls.py │ │ ├── api_views.py │ │ ├── apps.py │ │ ├── forms.py │ │ ├── middleware.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── permissions.py │ │ ├── serializers.py │ │ ├── tasks.py │ │ ├── templates │ │ │ └── authentication │ │ │ │ └── detailVCard.html │ │ ├── tests.py │ │ ├── urls.py │ │ └── views.py │ ├── main │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── api_views.py │ │ ├── apps.py │ │ ├── forms.py │ │ ├── management │ │ │ ├── __init__.py │ │ │ └── commands │ │ │ │ ├── __init__.py │ │ │ │ ├── clear-cache.py │ │ │ │ └── init-basic-data.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── permissions.py │ │ ├── serializers.py │ │ ├── tasks.py │ │ ├── templates │ │ │ ├── crossdomain.xml │ │ │ └── main │ │ │ │ ├── about.html │ │ │ │ ├── index.html │ │ │ │ └── landing.html │ │ ├── tests.py │ │ ├── urls.py │ │ └── views.py │ └── utils │ │ ├── __init__.py │ │ ├── pagination.py │ │ ├── permissions.py │ │ └── timezoneMiddleware.py ├── config │ ├── __init__.py │ ├── runner.py │ ├── settings │ │ ├── __init__.py │ │ ├── base.py │ │ ├── development.py │ │ ├── docker.py │ │ ├── production.py │ │ ├── sample-docker.env │ │ ├── sample-local.env │ │ ├── sample-production.env │ │ ├── sample-test.env │ │ └── test.py │ ├── urls.py │ └── wsgi.py ├── local-env.sh ├── locale │ └── README.md ├── logs │ └── .keep ├── manage.py ├── pytest.ini ├── requirements.txt ├── requirements │ ├── base.in │ ├── base.txt │ ├── development.txt │ ├── docker.txt │ └── production.txt ├── runserver.sh ├── runtest.sh ├── server-init.sh ├── templates │ ├── account │ │ ├── login.html │ │ ├── logout.html │ │ └── signup.html │ ├── base.html │ ├── form.html │ └── robots.txt └── wsgi.conf ├── staticfiles └── README.md ├── tasks.py └── webapp ├── .babelrc ├── .editorconfig ├── .gitattributes ├── .gitignore ├── Dockerfile ├── bower.json ├── gulpfile.babel.js ├── package-lock.json ├── package.json ├── src ├── app │ ├── apple-touch-icon.png │ ├── favicon.ico │ ├── robots.txt │ ├── scripts │ │ └── main.js │ └── styles │ │ └── main.scss └── index.html └── test ├── index.html └── spec └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | 3 | # Elastic Beanstalk Files 4 | .elasticbeanstalk/* 5 | !.elasticbeanstalk/*.cfg.yml 6 | !.elasticbeanstalk/*.global.yml 7 | 8 | .cache/ 9 | .coverage 10 | htmlcov/ 11 | 12 | *.sqlite3 13 | *.pyc 14 | .env 15 | .production.env 16 | .docker.env 17 | .local.env 18 | .test.env 19 | *.log 20 | staticfiles/* 21 | !staticfiles/README.md 22 | server/templates/dist/ 23 | .awspublish-*-statics 24 | 25 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [isort] 2 | default_section = THIRDPARTY 3 | line_length = 120 4 | known_first_party = apps 5 | known_first_external = archfx_cloud 6 | arch_factory 7 | iotile_cloud 8 | known_django = django 9 | known_django_related = allauth 10 | corsheaders 11 | crispy_forms 12 | django-pandas 13 | django_amazon_ses 14 | django_celery_beat 15 | django_elasticsearch_dsl 16 | django_filters 17 | django_json_widget 18 | django_permissions_policy 19 | django_redis 20 | drf_yasg 21 | environ 22 | health_check 23 | honeypot 24 | oauth2_provider 25 | rest_auth 26 | rest_framework 27 | rest_framework_simplejwt 28 | rest_pandas 29 | storages 30 | profile = django 31 | multi_line_output = 5 32 | use_parentheses = True 33 | combine_as_imports = False 34 | sections = FUTURE 35 | STDLIB 36 | THIRDPARTY 37 | DJANGO 38 | DJANGO_RELATED 39 | FIRST_EXTERNAL 40 | FIRSTPARTY 41 | LOCALFOLDER 42 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.8" 4 | install: 5 | - pip install -r requirements.txt 6 | - cd .. 7 | - django-admin.py startproject --template=./django-aws-template --extension=py,md,html,env my_proj 8 | - cd my_proj/server 9 | - cp config/settings/local.sample.env config/settings/.local.env 10 | - python manage.py migrate 11 | script: 12 | - python manage.py test profiles -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "~/.virtualenv/django-aws-template/bin/python", 3 | "python.linting.pylintArgs": [ 4 | "--load-plugins", 5 | "pylint_django" 6 | ], 7 | "python.testing.unittestEnabled": false, 8 | "python.testing.pytestEnabled": true, 9 | "python.testing.nosetestsEnabled": false, 10 | "python.testing.pytestArgs": [ 11 | "--exitfirst", 12 | "--verbose", 13 | "--rootdir", "server/", 14 | "server/apps" 15 | ], 16 | "python.linting.pylintEnabled": true, 17 | "python.linting.enabled": true, 18 | "files.insertFinalNewline": true, 19 | "files.trimTrailingWhitespace": true, 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 David Karchmer 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | {% comment "This comment section will be deleted in the generated project" %} 2 | 3 | ## django-aws-template ## 4 | 5 | ### Build Status ### 6 | 7 | An opinionated Django project starter intended for people that will release to AWS. It assumes 8 | 9 | 1. Django 3.2+ 10 | 1. main django server will be released to AWS Elastic Beanstalk, 11 | 1. static files will be released to s3/cloudfront using a gulp based flow (not django collectstatics) 12 | 1. Use docker for development/testing 13 | 1. Use a common set of django packages for the basics. In particular, django-allauth for social authentication 14 | and djangorestframework for a rest API. 15 | 16 | ### Features ### 17 | 18 | - Ready Bootstrap-themed, gulp based web pages 19 | - User Registration/Sign up with social login support (using django-allauth) 20 | - Ready to provide rest api for all models (using djangorestframework) 21 | - Gulp based flow to build CSS/JS files and release directly to s3/cloudfront (based on `yo webapp`) 22 | - Better Security with 12-Factor recommendations 23 | - Logging/Debugging Helpers 24 | - Works on Python 3.8+ with Django 3.2+ 25 | 26 | ### Quick start: ### 27 | 28 | ``` 29 | $ python3 -m venv .virtualenv/my_proj 30 | $ . .virtualenv/my_proj/bin/activate 31 | $ pip install django 32 | $ python -m django startproject --template=https://github.com/dkarchmer/django-aws-template/archive/master.zip --extension=py,md,html,env,json,config my_proj 33 | ``` 34 | 35 | The do the following manual work: 36 | 37 | * Create a .ebextensions directory with decired Elastic Beanstalk options. See https://github.com/dkarchmer/django-aws-template/tree/master/server/.ebextensions 38 | 39 | * Search and replace `PROECT_NAME` with your own project name. 40 | * Search and replace `mydomain` with your own domain 41 | * Search and replace `mystaticbucket` with your own S3 Bucket Name 42 | * Search and replace `us-east-1` with your own AWS_REGION 43 | * Search and replace `myawsprofile` with your own profile. Use `default` if you created the default one from `aws configure` 44 | * Search and replace `mycloudfrontdistributionid` with your CloudFront Distribution ID 45 | * Search `need-value` and add the appropriate value based on your setup 46 | 47 | *Rest of this README will be copied to the generated project.* 48 | 49 | {% endcomment %} 50 | 51 | # {{ project_name }} # 52 | 53 | Project is built with Python using the Django Web Framework. 54 | It is based on the django-aws-template (https://github.com/dkarchmer/django-aws-template) 55 | 56 | This project has the following basic features: 57 | 58 | * Custom Authentication Model with django-allauth 59 | * Rest API 60 | 61 | ## Installation 62 | 63 | ### Assumptions 64 | 65 | You must have the following installed on your computer 66 | 67 | * Python 3.8 or greater 68 | * Docker and docker-compose 69 | 70 | If not using docker, the following dependencies are also needed: 71 | 72 | * nodeJS v10 73 | * gulp 74 | 75 | For MacOS, see https://gist.github.com/dkarchmer/d8124f3ae1aa498eea8f0d658be214a5 76 | 77 | 78 | ## Using Docker (preferred) 79 | 80 | While everything can be built and run natively on your computer, using Docker ensures you use a tested environment 81 | that is more likely to run on any computer. Installing the exact version of NodeJS, for example, is particularly 82 | challenging. 83 | 84 | Once you have docker and docker-compose installed (see instructions on docker web site), you will be able to build the next set of images. 85 | 86 | This project builds the top level Django template file and all static files using modern techniques to ensure all 87 | static files are minized and ready for a CDN. 88 | 89 | Before you can start, you need to create a .docker.env file on your server/config/settings directory: 90 | 91 | ``` 92 | $ cp server/config/settings/sample-docker.env server/config/settings/.docker.env 93 | ``` 94 | 95 | This creates a little extra complexity, but is not a big deal when using Docker. Follow the instructions below 96 | carefully: 97 | 98 | ### Building Static Files 99 | 100 | These steps have to be run at least once, and every time the webapp is changed or new django statics are added (e.g. 101 | a new version of a package is installed) 102 | 103 | ```bash 104 | inv build-statics 105 | ``` 106 | 107 | ### Running Unit Test with docker compose 108 | 109 | After the webapp static files have been build, Docker Compose can be used to run the unit test. 110 | 111 | ```bash 112 | inv test -a build 113 | inv test -a signoff 114 | inv test -a custom -p apps/main 115 | ``` 116 | 117 | ### Running local server with docker compose 118 | 119 | To run the local server to test on your local host, use docker compose like: 120 | 121 | ```bash 122 | inv run-local -a up 123 | inv run-local -a logs-web 124 | inv run-local -a makemigrations 125 | inv run-local -a migrate 126 | inv run-local -a down 127 | ``` 128 | 129 | And important thing to understand is that we are basically creating the `base.html` template used by Django so these file needs to be moved (moved by the Gulp flow) to the Django `/templates` directory, so Django treats it like any other template that you could have created. The difference is that rather than that base template to be under version control, it is produced by the Gulp flow. This means that every time you change that base template (or the static CSS/JS), you need to run gulp again so it is copied again to the `/templates` directory. If you don't do this, and you try to run the local django server (or deploy it to AWS EB), the Django views will error out with a "Template not found" error. 130 | 131 | Note also we that we only build our own front end dependencies using Gulp. But Django comes with its own static files (for the Admin pages, for example), and you may be using popular libraries like `djangorestframework` or `django-crisp` which may include their own static files. Because of this, you still need to run the normal Django `collectstatics` command. Note that the configuration in the settings file will make `collectstatics` copy all these files to the `/statics` directory, which is also where the `gulp` flow will copy the distribution files. `/statics` is the directory we ultimately release static files from. The top level. The toplevel `inv deploy-staics` uploads all these files to an S3 bucket to either service the static files from, or as source to your CloudWatch CDN. 132 | 133 | To collect Django statics without building the webapp, run: 134 | 135 | ``` 136 | inv run-local -a collectstatics 137 | ``` 138 | 139 | ### Run local server (Docker) 140 | 141 | ```bash 142 | inv run-local -a build 143 | inv run-local -a up 144 | inv run-local -a down 145 | ``` 146 | 147 | `init-basic-data` will create a super user with username=admin, email=env(INITIAL_ADMIN_EMAIL) and password=admin. 148 | Make sure you change the password right away. 149 | It also creates django-allauth SocialApp records for Facebook, Google and Twitter (to avoid later errors). You will have to modify these records (from admin pages) with your own secret keys, or remove these social networks from the settings. 150 | 151 | For the production server, I recommend you do NOT let elastic beanstalk create the database, and instead manually create an RDS instance. This is not done by default in this template, but you can find several comments explaining how to configure a standa-alone RDS instance when ready. 152 | 153 | ### Testing 154 | 155 | ```bash 156 | inv test -a signoff 157 | inv test -a custom -p apps/main 158 | ``` 159 | 160 | 161 | ## Elastic Beanstack Deployment 162 | 163 | Review all files under `server/.ebextensions`, and modify if needed. Note that many settings are 164 | commented out as they require your own AWS settings. For example, `03-loadbalancer.config` shows how you would configure your ACM based SSL certificate. `04_notifications.config` shows how you may want to confirgure the SNS notifications to use a preconfigured topic, rather than EB creating one for you. `02_ec2.config` shows how to configure EB to use a specifc IAM role or a specific security group. Also something you will want to do. 165 | 166 | For early development, the `create` command will ask *Elastic Beanstalk (EB)* to create and manage its own 167 | RDS Postgres database. This also means that when the *EB* environment is terminated, the database will be 168 | terminated as well. 169 | 170 | Once the models are more stable, and for sure for production, it is recommended that you create your own 171 | RDS database (outside *EB*), and simply tell Django to use that. The `.ebextensions/01_main.config` has 172 | a bunch of `RDS_` environment variables (commented out) to use for this. Simply enable them, and set the 173 | proper RDS address. 174 | 175 | Before you can start, you need to create a `.production.env` file with all your secrets: 176 | 177 | ``` 178 | $ cp server/config/settings/sample-production.env server/config/settings/.production.env 179 | ``` 180 | 181 | Because you are about to deploy, you must update that .production.env with your actual secrets and domain 182 | specific information. 183 | 184 | ### Creating the environment 185 | 186 | Make sure you have search for all instances of `mydomain` in the code and replace with the proper settings. 187 | Also make sure you have created your own `server/config/settings/.production.env` based on the 188 | `sample-production.env` file. 189 | 190 | Look for the `EDIT` comments in `tasks.py` and `gulpfile.js` and edit as needed. 191 | 192 | After your have done all the required editing, the `create` Invoke command will run *Gulp* to deploy all static files, 193 | and then do the `eb init` and `eb create`: 194 | 195 | ``` 196 | invoke create 197 | ``` 198 | 199 | ### Deployment (development cycle) 200 | 201 | After your have created the environment, you can deploy code changes with the following command (which will run *Gulp* 202 | and `eb deploy`): 203 | 204 | ``` 205 | inv build-statics deploy-statics 206 | inv deploy 207 | ``` 208 | 209 | # Updating requirements 210 | 211 | This projects use pip-tools to manage requirements. Lists of required packages for each environment are located in *.in files, and complete pinned *.txt files are compiled from them with pip-compile command: 212 | 213 | ```bash 214 | cd server 215 | pip-compile requirements/base.in 216 | pip-compile requirements/development.in 217 | ``` 218 | 219 | To update dependency (e.g django) run following: 220 | 221 | ```bash 222 | pip-compile --upgrade-package django==3.1 requirements/base.in 223 | pip-compile --upgrade-package django==3.1 requirements/development.in 224 | ``` 225 | -------------------------------------------------------------------------------- /build-webapp.sh: -------------------------------------------------------------------------------- 1 | docker build -t webapp/builder webapp 2 | 3 | docker run --rm -v ${PWD}/webapp:/var/app/webapp -t webapp/builder rm -rf /var/app/webapp/node_modules 4 | docker run --rm -v ${PWD}/staticfiles:/var/app/staticfiles -t webapp/builder rm -rf /var/app/staticfiles/admin 5 | docker run --rm -v ${PWD}/staticfiles:/var/app/staticfiles -t webapp/builder rm -rf /var/app/staticfiles/debug_toolbar 6 | docker run --rm -v ${PWD}/staticfiles:/var/app/staticfiles -t webapp/builder rm -rf /var/app/staticfiles/rest_framework 7 | docker run --rm -v ${PWD}/server:/var/app/server -t webapp/builder rm -rf /var/app/server/templates/dist 8 | docker run --rm -v ${PWD}/server:/var/app/server -t webapp/builder mkdir /var/app/server/templates/dist /var/app/server/templates/dist/webapp 9 | 10 | docker run --rm -v ${PWD}/webapp:/var/app/webapp -v ${PWD}/server:/var/app/server -v ${PWD}/staticfiles:/var/app/staticfiles -t webapp/builder npm install 11 | docker run --rm -v ${PWD}/webapp:/var/app/webapp -v ${PWD}/server:/var/app/server -v ${PWD}/staticfiles:/var/app/staticfiles -t webapp/builder gulp 12 | -------------------------------------------------------------------------------- /buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.1 2 | 3 | environment_variables: 4 | plaintext: 5 | DJANGO_SETTINGS_MODULE: config.settings.test 6 | SECRET_KEY: nosecret 7 | DATABASE_DEFAULT_URL: sqlite:///db1.sqlite3 8 | DATABASE_STREAMDATA_URL: sqlite:///db2.sqlite3 9 | STREAM_INCOMING_PRIVATE_KEY: changeme 10 | STREAM_INCOMING_PUBLIC_KEY: changeme 11 | GOOGLE_API_KEY: changeme 12 | OPBEAT_ENABLED: False 13 | 14 | phases: 15 | install: 16 | commands: 17 | - apt-get update -y 18 | - apt-get install -y maven curl wget 19 | - apt-get purge nodejs npm 20 | - curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash - 21 | - apt-get install -y nodejs 22 | - node --version 23 | pre_build: 24 | commands: 25 | - pip install -r server/requirements/development.txt 26 | - cd webapp && npm install & cd .. 27 | - cd webapp && gulp && cd .. 28 | - cd server && python manage.py collectstatic --noinput && cd .. 29 | build: 30 | commands: 31 | - cd server && coverage run -m py.test > test.out && cd .. 32 | - cd server && coverage report --include=apps/* > coverage.out && cd .. 33 | - cd server && coverage html --include=apps/* && cd .. 34 | post_build: 35 | commands: 36 | - echo Build completed on `date` 37 | -------------------------------------------------------------------------------- /contributors.txt: -------------------------------------------------------------------------------- 1 | David Karchmer 2 | -------------------------------------------------------------------------------- /docker-compose.utest.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | 4 | web: 5 | build: 6 | context: ./server/ 7 | volumes: 8 | - ./logs:/var/app/logs 9 | - ./server:/var/app 10 | - ./staticfiles:/www/static 11 | ports: 12 | - "8080:8080" 13 | environment: 14 | - DJANGO_SETTINGS_MODULE=config.settings.development 15 | - DJANGO_ENV_FILE=.test.env 16 | - DJANGO_SERVER_MODE=Test 17 | - DJANGO_SECRET_KEY=test-dummy-key1 18 | - DATABASE_URL=sqlite:///db.sqlite3 19 | - SECRET_KEY=nosecret 20 | - PRODUCTION=False 21 | - DEBUG=True 22 | - DOMAIN_NAME=127.0.0.1:8000 23 | - DOMAIN_BASE_URL=http://127.0.0.1:8000 24 | - COMPANY_NAME=Test Corp 25 | - INITIAL_ADMIN_EMAIL=admin@test.com 26 | - AWS_ACCESS_KEY_ID=need-value 27 | - AWS_SECRET_ACCESS_KEY=need-value 28 | command: sh runtest.sh 29 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | x-server-env: &server-env 4 | DJANGO_SETTINGS_MODULE: config.settings.docker 5 | DJANGO_ENV_FILE: .docker.env 6 | DJANGO_SECRET_KEY: stage-dummy-key1 7 | SECRET_KEY: stage-dummy-key1 8 | DATABASE_URL: postgres://postgres:postgres@db/postgres 9 | RDS_DB_NAME: ${RDS_DB_NAME:-postgres} 10 | RDS_USERNAME: ${RDS_USERNAME:-postgres} 11 | RDS_PASSWORD: ${RDS_PASSWORD:-pass.123} 12 | RDS_HOSTNAME: ${RDS_HOSTNAME:-db} 13 | RDS_PORT: 5432 14 | 15 | services: 16 | 17 | db: 18 | restart: always 19 | image: postgres:11.8 20 | environment: 21 | - POSTGRES_PASSWORD=postgres 22 | volumes: 23 | - /var/lib/postgresql/data 24 | - /data 25 | 26 | nginx: 27 | restart: always 28 | build: ./nginx/ 29 | ports: 30 | - "80:80" 31 | volumes: 32 | - ./staticfiles:/www/static 33 | depends_on: 34 | - web 35 | 36 | web: 37 | build: 38 | context: ./server/ 39 | volumes: 40 | - ./logs:/var/app/logs 41 | - ./server:/var/app 42 | - ./staticfiles:/www/static 43 | ports: 44 | - "8000:8000" 45 | command: /var/app/runserver.sh 46 | depends_on: 47 | - db 48 | environment: 49 | <<: *server-env 50 | 51 | init: 52 | build: 53 | context: ./server/ 54 | volumes: 55 | - ./logs:/var/app/logs 56 | - ./server:/var/app 57 | - ./staticfiles:/www/static 58 | command: ["sh", "-c", "/var/app/server-init.sh"] 59 | restart: "no" 60 | depends_on: 61 | - db 62 | environment: 63 | <<: *server-env 64 | -------------------------------------------------------------------------------- /logs/README.md: -------------------------------------------------------------------------------- 1 | * `django.log`: Contains logs by Django framework like executed SQL statements 2 | * `project.log`: Contains logs from the `project` logger. For example: 3 | 4 | # At the top of your file/module 5 | import logging 6 | logger = logging.getLogger("project") 7 | 8 | # Anywhere else in the file 9 | logger.info('Started processing foo') -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tutum/nginx 2 | RUN rm /etc/nginx/sites-enabled/default 3 | ADD sites-enabled/ /etc/nginx/sites-enabled 4 | -------------------------------------------------------------------------------- /nginx/sites-enabled/django_project: -------------------------------------------------------------------------------- 1 | server { 2 | 3 | listen 80; 4 | server_name mydomain.com; 5 | charset utf-8; 6 | 7 | location /static { 8 | alias /www/static; 9 | } 10 | 11 | location / { 12 | proxy_pass http://web:8000; 13 | proxy_set_header Host $host; 14 | proxy_set_header X-Real-IP $remote_addr; 15 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | awsebcli>=3.12.4 2 | invoke>=1.6.0 3 | isort 4 | pip-tools 5 | -------------------------------------------------------------------------------- /server/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | *migrations* 4 | *tests.py 5 | *manage.py 6 | *config* 7 | *apps.py 8 | *management* 9 | -------------------------------------------------------------------------------- /server/.ebextensions/01_main.config: -------------------------------------------------------------------------------- 1 | packages: 2 | yum: 3 | postgresql94-devel: [] 4 | 5 | container_commands: 6 | 01_migrate: 7 | command: "django-admin.py migrate --noinput" 8 | leader_only: true 9 | 02_init_data: 10 | command: "django-admin.py init-basic-data" 11 | leader_only: true 12 | 03_wsgireplace: 13 | command: 'cp wsgi.conf ../wsgi.conf' 14 | 15 | option_settings: 16 | - namespace: aws:elasticbeanstalk:container:python 17 | option_name: WSGIPath 18 | value: config/wsgi.py 19 | - option_name: DJANGO_SETTINGS_MODULE 20 | value: config.settings.production 21 | - option_name: DJANGO_SECRET_KEY 22 | value: {{ secret_key }} 23 | - option_name: DJANGO_ENV_FILE 24 | value: .production.env 25 | - option_name: PRODUCTION 26 | value: True 27 | ## Database ENV Vars 28 | ## Enable when switching to a stand-alone RDS instance (outside EB) 29 | #- option_name: RDS_DB_NAME 30 | # value: mydb 31 | #- option_name: RDS_USERNAME 32 | # value: ebroot 33 | #- option_name: RDS_PASSWORD 34 | # value: secret%Password 35 | #- option_name: RDS_HOSTNAME 36 | # value: name.xxxxxxxxx.us-east-1.rds.amazonaws.com 37 | #- option_name: RDS_PORT 38 | # value: 5432 39 | 40 | -------------------------------------------------------------------------------- /server/.ebextensions/02_ec2.config: -------------------------------------------------------------------------------- 1 | option_settings: 2 | ## Go to AWS Console and request an EC2 ssh key 3 | #- namespace: aws:autoscaling:launchconfiguration 4 | # option_name: EC2KeyName 5 | # value: my-ssh-key 6 | ## Enable if you want to use your own AIM Policy 7 | #- namespace: aws:autoscaling:launchconfiguration 8 | # option_name: IamInstanceProfile 9 | # value: arn:aws:iam::xxxxxxxxxxxxxx:instance-profile/........ 10 | #- namespace: aws:autoscaling:launchconfiguration 11 | # option_name: SecurityGroups 12 | # value: 'db-source,elb-node' 13 | - namespace: aws:autoscaling:launchconfiguration 14 | option_name: InstanceType 15 | value: t2.micro 16 | -------------------------------------------------------------------------------- /server/.ebextensions/03-loadbalancer.config: -------------------------------------------------------------------------------- 1 | option_settings: 2 | # Elastic Load Balancer Options 3 | - namespace: aws:elb:loadbalancer 4 | option_name: LoadBalancerHTTPPort 5 | value: 80 6 | - namespace: aws:elb:loadbalancer 7 | option_name: LoadBalancerPortProtocol 8 | value: HTTP 9 | ## Enable when you get an AWS ACM (or equivalent) SSL Certificate 10 | #- namespace: aws:elb:loadbalancer 11 | # option_name: LoadBalancerHTTPSPort 12 | # value: 443 13 | #- namespace: aws:elb:loadbalancer 14 | # option_name: LoadBalancerSSLPortProtocol 15 | # value: HTTPS 16 | #- namespace: aws:elb:loadbalancer 17 | # option_name: SSLCertificateId 18 | # value: arn:aws:acm:us-east-1:xxxxxxxxxxxxxxx:certificate/.................... 19 | - namespace: aws:elasticbeanstalk:application 20 | option_name: Application Healthcheck URL 21 | value: /about/ 22 | - namespace: aws:autoscaling:asg 23 | option_name: MinSize 24 | value: 1 25 | - namespace: aws:autoscaling:asg 26 | option_name: MaxSize 27 | value: 4 28 | -------------------------------------------------------------------------------- /server/.ebextensions/04_notifications.config: -------------------------------------------------------------------------------- 1 | option_settings: 2 | ## Enable if you have your own SNS Topic you want to use 3 | # Notification Options, Use existing SNS 4 | #- namespace: aws:elasticbeanstalk:sns:topics 5 | # option_name: Notification Topic ARN 6 | # value: arn:aws:sns:us-east-1:xxxxxxxxxxxxxxx:.................. 7 | - namespace: aws:elasticbeanstalk:sns:topics 8 | option_name: Notification Topic Name 9 | value: ElasticBeanstalkNotifications 10 | -------------------------------------------------------------------------------- /server/.ebextensions/05_logs.config: -------------------------------------------------------------------------------- 1 | files: 2 | "/opt/python/log/my.log" : 3 | mode: "000666" 4 | owner: ec2-user 5 | group: ec2-user 6 | content: | 7 | # Django log file -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim-buster 2 | 3 | ENV C_FORCE_ROOT 1 4 | 5 | # create unprivileged user 6 | RUN adduser --disabled-password --gecos '' myuser 7 | 8 | # Install PostgreSQL dependencies 9 | RUN apt-get update && \ 10 | apt-get install -y postgresql-client libpq-dev supervisor netcat gcc 11 | 12 | # Step 1: Install any Python packages 13 | # ---------------------------------------- 14 | 15 | ENV PYTHONUNBUFFERED 1 16 | RUN mkdir /var/app 17 | WORKDIR /var/app 18 | COPY requirements /var/app/requirements 19 | RUN pip install -U pip 20 | RUN pip install -r requirements/docker.txt 21 | RUN pip install gunicorn 22 | 23 | # Step 2: Copy Django Code 24 | # ---------------------------------------- 25 | 26 | COPY apps /var/app/apps 27 | COPY config /var/app/config 28 | COPY manage.py /var/app/manage.py 29 | ADD runserver.sh /var/app/runserver.sh 30 | ADD server-init.sh /var/app/server-init.sh 31 | ADD runtest.sh /var/app/runtest.sh 32 | RUN mkdir /var/app/logs 33 | COPY locale /var/app/locale 34 | 35 | CMD ["/var/app/runserver.sh"] 36 | -------------------------------------------------------------------------------- /server/apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkarchmer/django-aws-template/2bf15740cdad0ff9e6656c3c452a5ee6042cc0a6/server/apps/__init__.py -------------------------------------------------------------------------------- /server/apps/authentication/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkarchmer/django-aws-template/2bf15740cdad0ff9e6656c3c452a5ee6042cc0a6/server/apps/authentication/__init__.py -------------------------------------------------------------------------------- /server/apps/authentication/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .forms import AdminUserChangeForm, AdminUserCreationForm 4 | from .models import * 5 | 6 | 7 | class AccountAdmin(admin.ModelAdmin): 8 | 9 | def get_form(self, request, obj=None, **kwargs): 10 | 11 | if obj: 12 | return AdminUserChangeForm 13 | else: 14 | return AdminUserCreationForm 15 | 16 | list_display = ('id', 'username', 'email', 'name', 'created_at', 'last_login', 'is_active', 'is_staff') 17 | 18 | 19 | 20 | """ 21 | Register Admin Pages 22 | """ 23 | admin.site.register(Account, AccountAdmin) 24 | -------------------------------------------------------------------------------- /server/apps/authentication/api_urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from django.views.decorators.csrf import csrf_exempt 3 | 4 | from rest_framework.authtoken.views import obtain_auth_token 5 | 6 | from .api_views import APILoginViewSet, APILogoutViewSet, APITokenViewSet, APIUserInfoViewSet, FacebookLoginOrSignup 7 | 8 | urlpatterns = [ 9 | path('login/', APILoginViewSet.as_view(), name='api-login'), 10 | path('logout/', APILogoutViewSet.as_view(), name='api-logout'), 11 | path('token/', APITokenViewSet.as_view(), name='api-token'), 12 | path('user-info/', APIUserInfoViewSet.as_view(), name='api-user-info'), 13 | path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), 14 | path('api-token-auth/', csrf_exempt(obtain_auth_token), name='api-token-auth'), 15 | path('facebook-signup/?', csrf_exempt(FacebookLoginOrSignup.as_view()), name='facebook-login-signup'), 16 | ] 17 | -------------------------------------------------------------------------------- /server/apps/authentication/api_views.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | 4 | from django.conf import settings 5 | from django.contrib.auth import authenticate, login, logout 6 | from django.http import HttpResponse 7 | from django.views.decorators.csrf import csrf_exempt 8 | 9 | from allauth.socialaccount.helpers import complete_social_login 10 | from allauth.socialaccount.models import SocialApp, SocialLogin, SocialToken 11 | from allauth.socialaccount.providers.facebook.views import fb_complete_login 12 | from rest_framework import permissions, status, views, viewsets 13 | from rest_framework.authentication import SessionAuthentication 14 | from rest_framework.authtoken.models import Token 15 | from rest_framework.decorators import action, permission_classes 16 | from rest_framework.parsers import JSONParser 17 | from rest_framework.permissions import AllowAny 18 | from rest_framework.renderers import JSONRenderer 19 | from rest_framework.response import Response 20 | from rest_framework.views import APIView 21 | 22 | from apps.utils.permissions import * 23 | 24 | from .models import Account 25 | from .permissions import IsAccountOwner 26 | from .serializers import * 27 | from .tasks import send_new_user_notification 28 | 29 | # Get an instance of a logger 30 | logger = logging.getLogger(__name__) 31 | 32 | 33 | class APITokenViewSet(APIView): 34 | """ 35 | View to get User's token 36 | """ 37 | 38 | def get(self, request, format=None): 39 | """ 40 | Update thumbnail and tiny file field 41 | """ 42 | if request.user.is_anonymous: 43 | # User most login before they can get a token 44 | # This not only ensures the user has registered, and has an account 45 | # but that the account is active 46 | return Response('User not recognized.', status=status.HTTP_403_FORBIDDEN) 47 | 48 | data_dic = {} 49 | 50 | try: 51 | token = Token.objects.get(user=request.user) 52 | mystatus = status.HTTP_200_OK 53 | except: 54 | token = Token.objects.create(user=request.user) 55 | mystatus = status.HTTP_201_CREATED 56 | 57 | data_dic['token'] = token.key 58 | return Response(data_dic, status=mystatus) 59 | 60 | 61 | class AccountViewSet(viewsets.ModelViewSet): 62 | lookup_field = 'username' 63 | queryset = Account.objects.all() 64 | serializer_class = AccountSerializer 65 | 66 | def get_permissions(self): 67 | 68 | if self.request.method in permissions.SAFE_METHODS: 69 | return (permissions.IsAuthenticated(),) 70 | 71 | if self.request.method == 'POST': 72 | return (permissions.AllowAny(),) 73 | 74 | return (permissions.IsAuthenticated(), IsAccountOwner(),) 75 | 76 | @csrf_exempt 77 | def create(self, request): 78 | ''' 79 | When you create an object using the serializer's .save() method, the 80 | object's attributes are set literally. This means that a user registering with 81 | the password 'password' will have their password stored as 'password'. This is bad 82 | for a couple of reasons: 1) Storing passwords in plain text is a massive security 83 | issue. 2) Django hashes and salts passwords before comparing them, so the user 84 | wouldn't be able to log in using 'password' as their password. 85 | 86 | We solve this problem by overriding the .create() method for this viewset and 87 | using Account.objects.create_user() to create the Account object. 88 | ''' 89 | 90 | serializer = self.serializer_class(data=request.data) 91 | 92 | if serializer.is_valid(): 93 | password = serializer.validated_data['password'] 94 | confirm_password = serializer.validated_data['confirm_password'] 95 | 96 | if password and confirm_password and password == confirm_password: 97 | 98 | # Note that for now, Accounts default to is_active=False 99 | # which means that we need to manually active them 100 | # This is to keep the site secure until we go live 101 | account = Account.objects.create_user(**serializer.validated_data) 102 | 103 | account.set_password(serializer.validated_data['password']) 104 | account.save() 105 | 106 | # For now, we also want to email Admin every time anybody registers 107 | send_new_user_notification(id=account.id, username=account.username, email=account.email) 108 | 109 | return Response(serializer.validated_data, status=status.HTTP_201_CREATED) 110 | 111 | return Response({'status': 'Bad request', 112 | 'message': 'Account could not be created with received data.' 113 | }, status=status.HTTP_400_BAD_REQUEST) 114 | 115 | @action(methods=['post'], detail=True) 116 | def set_password(self, request, username=None): 117 | account = self.get_object() 118 | serializer = PasswordCustomSerializer(data=request.data) 119 | if serializer.is_valid(): 120 | account.set_password(serializer.data['password']) 121 | account.save() 122 | 123 | return Response({'status': 'password set'}, status=status.HTTP_201_CREATED) 124 | else: 125 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 126 | 127 | 128 | class APILoginViewSet(APIView): 129 | """ 130 | View to list all users in the system. 131 | 132 | * Requires token authentication. 133 | * Only admin users are able to access this view. 134 | """ 135 | #permission_classes = () 136 | 137 | @csrf_exempt 138 | def post(self, request, format=None): 139 | """ 140 | Update thumbnail and tiny file field 141 | """ 142 | data = JSONParser().parse(request) 143 | serializer = LoginCustomSerializer(data=data) 144 | 145 | if serializer.is_valid(): 146 | email = serializer.data.get('email') 147 | password = serializer.data.get('password') 148 | 149 | if not request.user.is_anonymous: 150 | return Response('Already Logged-in', status=status.HTTP_403_FORBIDDEN) 151 | 152 | account = authenticate(email=email, password=password) 153 | 154 | if account is not None: 155 | if account.is_active: 156 | login(request, account) 157 | 158 | serialized = AccountSerializer(account) 159 | data = serialized.data 160 | 161 | # Add the token to the return serialization 162 | try: 163 | token = Token.objects.get(user=account) 164 | except: 165 | token = Token.objects.create(user=account) 166 | 167 | data['token'] = token.key 168 | 169 | 170 | return Response(data) 171 | else: 172 | return Response('This account is not Active.', status=status.HTTP_401_UNAUTHORIZED) 173 | else: 174 | return Response('Username/password combination invalid.', status=status.HTTP_401_UNAUTHORIZED) 175 | 176 | return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 177 | 178 | def get(self, request, format=None): 179 | """ 180 | Update thumbnail and tiny file field 181 | """ 182 | 183 | data_dic = {"Error":"GET not supported for this command"} 184 | 185 | logout(request) 186 | mystatus = status.HTTP_400_BAD_REQUEST 187 | 188 | return Response(data_dic, status=mystatus) 189 | 190 | 191 | class APILogoutViewSet(APIView): 192 | permission_classes = (permissions.IsAuthenticated,) 193 | 194 | def post(self, request, format=None): 195 | logout(request) 196 | 197 | return Response({}, status=status.HTTP_204_NO_CONTENT) 198 | 199 | 200 | class APIUserInfoViewSet(APIView): 201 | """ 202 | View to list all users in the system. 203 | 204 | * Requires token authentication. 205 | * Only admin users are able to access this view. 206 | """ 207 | #permission_classes = () 208 | 209 | def get(self, request, format=None): 210 | """ 211 | Update thumbnail and tiny file field 212 | """ 213 | if request.user.is_anonymous: 214 | # User most login before they can get a token 215 | # This not only ensures the user has registered, and has an account 216 | # but that the account is active 217 | return Response('User not recognized.', status=status.HTTP_403_FORBIDDEN) 218 | 219 | account = request.user 220 | 221 | serialized = AccountSerializer(account) 222 | data = serialized.data 223 | 224 | # Add the token to the return serialization 225 | try: 226 | token = Token.objects.get(user=account) 227 | except: 228 | token = Token.objects.create(user=account) 229 | 230 | data['token'] = token.key 231 | 232 | return Response(data) 233 | 234 | 235 | class EverybodyCanAuthentication(SessionAuthentication): 236 | def authenticate(self, request): 237 | return None 238 | 239 | 240 | # Add a user to the system based on facebook token 241 | class FacebookLoginOrSignup(APIView): 242 | 243 | permission_classes = (AllowAny,) 244 | 245 | # this is a public api!!! 246 | authentication_classes = (EverybodyCanAuthentication,) 247 | 248 | def dispatch(self, *args, **kwargs): 249 | return super(FacebookLoginOrSignup, self).dispatch(*args, **kwargs) 250 | 251 | def post(self, request): 252 | data = JSONParser().parse(request) 253 | access_token = data.get('access_token', '') 254 | 255 | try: 256 | app = SocialApp.objects.get(provider="facebook") 257 | token = SocialToken(app=app, token=access_token) 258 | 259 | # check token against facebook 260 | login = fb_complete_login(app, token) 261 | login.token = token 262 | login.state = SocialLogin.state_from_request(request) 263 | 264 | # add or update the user into users table 265 | ret = complete_social_login(request, login) 266 | 267 | # if we get here we've succeeded 268 | return Response(status=200, data={ 269 | 'success': True, 270 | 'username': request.user.username, 271 | 'user_id': request.user.pk, 272 | }) 273 | 274 | except: 275 | 276 | return Response(status=401 ,data={ 277 | 'success': False, 278 | 'reason': "Bad Access Token", 279 | }) 280 | 281 | -------------------------------------------------------------------------------- /server/apps/authentication/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountConfig(AppConfig): 5 | name = 'apps.authentication' 6 | -------------------------------------------------------------------------------- /server/apps/authentication/forms.py: -------------------------------------------------------------------------------- 1 | import pytz 2 | from captcha.fields import ReCaptchaField 3 | 4 | from django import forms as forms 5 | from django.conf import settings 6 | from django.contrib.auth.forms import ReadOnlyPasswordHashField 7 | from django.core.exceptions import ValidationError 8 | from django.forms import ModelForm 9 | 10 | from allauth.account.forms import LoginForm, SignupForm 11 | from crispy_forms.helper import FormHelper 12 | from crispy_forms.layout import Field, Layout, Submit 13 | 14 | from .models import Account 15 | 16 | 17 | class TimeZoneFormField(forms.TypedChoiceField): 18 | def __init__(self, *args, **kwargs): 19 | 20 | def coerce_to_pytz(val): 21 | try: 22 | return pytz.timezone(val) 23 | except pytz.UnknownTimeZoneError: 24 | raise ValidationError("Unknown time zone: '%s'" % val) 25 | 26 | defaults = { 27 | 'coerce': coerce_to_pytz, 28 | 'choices': [(tz, tz) for tz in pytz.common_timezones], 29 | 'empty_value': None, 30 | } 31 | defaults.update(kwargs) 32 | super(TimeZoneFormField, self).__init__(*args, **defaults) 33 | 34 | 35 | class AccountUpdateForm(ModelForm): 36 | time_zone = TimeZoneFormField() 37 | class Meta: 38 | model = Account 39 | fields = ['username', 'name', 'tagline'] 40 | 41 | def __init__(self, *args, **kwargs): 42 | super(AccountUpdateForm, self).__init__(*args, **kwargs) 43 | self.fields['username'].required = True 44 | self.fields['username'].widget.attrs['class'] = 'custom-form-element custom-input-text' 45 | self.fields['name'].widget.attrs['class'] = 'custom-form-element custom-input-text' 46 | self.fields['tagline'].widget.attrs['class'] = 'custom-form-element custom-input-text' 47 | tz = settings.TIME_ZONE 48 | if 'instance' in kwargs: 49 | user = kwargs['instance'] 50 | if user and user.time_zone: 51 | tz = user.time_zone 52 | self.initial['time_zone'] = tz 53 | self.fields['time_zone'].widget.attrs['class'] = 'custom-form-element custom-select' 54 | 55 | 56 | class AllauthSignupForm(SignupForm): 57 | """Base form for django-allauth to use, adding a ReCaptcha function""" 58 | 59 | # captcha = ReCaptchaField() 60 | 61 | def __init__(self, *args, **kwargs): 62 | super(AllauthSignupForm, self).__init__(*args, **kwargs) 63 | self.helper = FormHelper() 64 | self.helper.form_method = 'post' 65 | self.helper.add_input(Submit('submit', 'Sign Up', css_class='btn btn-lg btn-success btn-block')) 66 | 67 | 68 | class AllauthLoginForm(LoginForm): 69 | def __init__(self, *args, **kwargs): 70 | super(AllauthLoginForm, self).__init__(*args, **kwargs) 71 | self.fields['password'].widget = forms.PasswordInput() 72 | 73 | self.helper = FormHelper() 74 | self.helper.form_method = 'post' 75 | self.helper.add_input(Submit('submit', 'Sign In', css_class='btn btn-lg btn-success btn-block')) 76 | 77 | 78 | class AdminUserCreationForm(forms.ModelForm): 79 | """A form for creating new users. Includes all the required 80 | fields, plus a repeated password.""" 81 | password1 = forms.CharField(label='Password', widget=forms.PasswordInput) 82 | password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput) 83 | 84 | class Meta: 85 | model = Account 86 | fields = ('username', 'email', 'name', 'is_staff', ) 87 | 88 | def clean_password2(self): 89 | # Check that the two password entries match 90 | password1 = self.cleaned_data.get("password1") 91 | password2 = self.cleaned_data.get("password2") 92 | if password1 and password2 and password1 != password2: 93 | raise forms.ValidationError("Passwords don't match") 94 | return password2 95 | 96 | def save(self, commit=True): 97 | # Save the provided password in hashed format 98 | user = super(AdminUserCreationForm, self).save(commit=False) 99 | user.set_password(self.cleaned_data["password1"]) 100 | if commit: 101 | user.save() 102 | return user 103 | 104 | 105 | class AdminUserChangeForm(forms.ModelForm): 106 | """A form for updating users. Includes all the fields on 107 | the user, but replaces the password field with admin's 108 | password hash display field. 109 | """ 110 | password = ReadOnlyPasswordHashField() 111 | 112 | class Meta: 113 | model = Account 114 | fields = ('username', 'email', 'password', 'name', 'is_active', 'is_staff', 'is_admin') 115 | 116 | def clean_password(self): 117 | # Regardless of what the user provides, return the initial value. 118 | # This is done here, rather than on the field, because the 119 | # field does not have access to the initial value 120 | return self.initial["password"] -------------------------------------------------------------------------------- /server/apps/authentication/middleware.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.contrib.auth import SESSION_KEY, get_user, get_user_model 4 | from django.contrib.auth.models import AnonymousUser 5 | from django.core.cache import caches 6 | from django.db.models.signals import post_delete, post_save 7 | from django.utils.functional import SimpleLazyObject 8 | 9 | CACHE_KEY = 'cached_user:{0}' 10 | 11 | # Get an instance of a logger 12 | logger = logging.getLogger(__name__) 13 | 14 | def invalidate_cache(sender, instance, **kwargs): 15 | if isinstance(instance, get_user_model()): 16 | key = CACHE_KEY.format(instance.id) 17 | else: 18 | key = CACHE_KEY.format(instance.user_id) 19 | logger.debug('Deleting User Cache: {0}'.format(key)) 20 | cache = caches['default'] 21 | cache.delete(key) 22 | 23 | 24 | def get_cached_user(request): 25 | if not hasattr(request, '_cached_user'): 26 | try: 27 | key = CACHE_KEY.format(request.session[SESSION_KEY]) 28 | cache = caches['default'] 29 | user = cache.get(key) 30 | except KeyError: 31 | user = AnonymousUser() 32 | if user is None: 33 | user = get_user(request) 34 | cache = caches['default'] 35 | # 8 hours 36 | cache.set(key, user, 28800) 37 | logger.debug('No User Cache. Setting now: {0}, {1}'.format(key, user.username)) 38 | request._cached_user = user 39 | return request._cached_user 40 | 41 | 42 | class CachedAuthenticationMiddleware(object): 43 | 44 | def __init__(self): 45 | post_save.connect(invalidate_cache, sender=get_user_model()) 46 | post_delete.connect(invalidate_cache, sender=get_user_model()) 47 | 48 | def process_request(self, request): 49 | request.user = SimpleLazyObject(lambda: get_cached_user(request)) 50 | -------------------------------------------------------------------------------- /server/apps/authentication/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.4 on 2016-03-16 22:32 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Account', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('password', models.CharField(max_length=128, verbose_name='password')), 21 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 22 | ('email', models.EmailField(max_length=254, unique=True)), 23 | ('username', models.CharField(max_length=40, unique=True)), 24 | ('slug', models.SlugField(max_length=60, unique=True)), 25 | ('name', models.CharField(blank=True, max_length=120, verbose_name='Full Name')), 26 | ('tagline', models.CharField(blank=True, max_length=260)), 27 | ('external_avatar_url', models.CharField(blank=True, max_length=260, null=True)), 28 | ('is_staff', models.BooleanField(default=False)), 29 | ('is_active', models.BooleanField(default=True)), 30 | ('is_admin', models.BooleanField(default=False)), 31 | ('time_zone', models.CharField(default='UTC', max_length=64, null=True)), 32 | ('created_at', models.DateTimeField(auto_now_add=True)), 33 | ('updated_at', models.DateTimeField(auto_now=True)), 34 | ], 35 | options={ 36 | 'abstract': False, 37 | }, 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /server/apps/authentication/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkarchmer/django-aws-template/2bf15740cdad0ff9e6656c3c452a5ee6042cc0a6/server/apps/authentication/migrations/__init__.py -------------------------------------------------------------------------------- /server/apps/authentication/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | Custom User Model 3 | """ 4 | 5 | import hashlib 6 | import logging 7 | import os 8 | # import code for encoding urls and generating md5 hashes 9 | import urllib.parse 10 | import uuid 11 | 12 | import pytz 13 | 14 | from django.conf import settings 15 | from django.contrib.auth.models import AbstractBaseUser, BaseUserManager 16 | from django.db import models 17 | from django.db.models import Q 18 | from django.template.defaultfilters import slugify 19 | 20 | from allauth.account.signals import user_signed_up 21 | from rest_framework.authtoken.models import Token 22 | 23 | from .tasks import send_new_user_notification 24 | 25 | # Get an instance of a logger 26 | logger = logging.getLogger(__name__) 27 | 28 | 29 | def new_sign_up(sender, **kwargs): 30 | account = kwargs['user'] 31 | logger.info('A new user has signed up! - {username}'.format(username=account.username)) 32 | logger.debug('--> New User: ' + str(account)) 33 | send_new_user_notification(id=account.id, username=account.username, email=account.email) 34 | 35 | ''' 36 | When a social account is created successfully and this signal is received, 37 | django-allauth passes in the sociallogin param, giving access to metadata on the remote account, e.g.: 38 | 39 | sociallogin.account.provider # e.g. 'twitter' 40 | sociallogin.account.get_avatar_url() 41 | sociallogin.account.get_profile_url() 42 | sociallogin.account.extra_data['screen_name'] 43 | 44 | See the socialaccount_socialaccount table for more in the 'extra_data' field. 45 | ''' 46 | 47 | if not 'sociallogin' in kwargs: 48 | return 49 | 50 | social_login = kwargs['sociallogin'] 51 | print('user is ' + str(account)) 52 | if social_login: 53 | print(str(social_login.account.provider)) 54 | print(str(social_login.account.extra_data)) 55 | # Extract first / last names from social nets and store on User record 56 | if social_login.account.provider == 'twitter': 57 | if 'name' in social_login.account.extra_data: 58 | account.name = social_login.account.extra_data['name'] 59 | 60 | if social_login.account.provider == 'facebook': 61 | name = '' 62 | if 'first_name' in social_login.account.extra_data: 63 | first_name = social_login.account.extra_data['first_name'] 64 | name = first_name 65 | if 'last_name' in social_login.account.extra_data: 66 | last_name = social_login.account.extra_data['last_name'] 67 | if name != '': 68 | name += ' ' 69 | name += last_name 70 | 71 | account.name = name 72 | 73 | if social_login.account.provider == 'google': 74 | if 'name' in social_login.account.extra_data: 75 | account.name = social_login.account.extra_data['name'] 76 | if 'picture' in social_login.account.extra_data: 77 | account.external_avatar_url = social_login.account.extra_data['picture'] 78 | 79 | account.save() 80 | 81 | 82 | # Connect django-allauth Signals 83 | user_signed_up.connect(new_sign_up) 84 | 85 | 86 | class AccountManager(BaseUserManager): 87 | def create_user(self, email, password=None, **kwargs): 88 | if not email: 89 | raise ValueError('Users must have a valid email address.') 90 | 91 | if not kwargs.get('username'): 92 | raise ValueError('Users must have a valid username.') 93 | 94 | account = self.model( 95 | email=self.normalize_email(email), username=kwargs.get('username') 96 | ) 97 | 98 | account.set_password(password) 99 | account.access_level = 0 100 | account.save() 101 | 102 | return account 103 | 104 | def create_superuser(self, email, password, **kwargs): 105 | account = self.create_user(email, password, **kwargs) 106 | 107 | account.is_admin = True 108 | account.is_active = True 109 | account.is_staff = True 110 | account.save() 111 | 112 | return account 113 | 114 | 115 | class Account(AbstractBaseUser): 116 | 117 | TZ_CHOICES = [(tz, tz) for tz in pytz.common_timezones] 118 | 119 | email = models.EmailField(unique=True) 120 | username = models.CharField(max_length=40, unique=True) 121 | slug = models.SlugField(max_length=60, unique=True) 122 | 123 | name = models.CharField(verbose_name='Full Name', max_length=120, blank=True) 124 | tagline = models.CharField(max_length=260, blank=True) 125 | 126 | external_avatar_url = models.CharField(max_length=260, blank=True, null=True) 127 | 128 | is_staff = models.BooleanField(default=False) 129 | is_active = models.BooleanField(default=True) 130 | is_admin = models.BooleanField(default=False) 131 | 132 | time_zone = models.CharField(max_length=64, null=True, default=settings.TIME_ZONE) 133 | 134 | created_at = models.DateTimeField(auto_now_add=True) 135 | updated_at = models.DateTimeField(auto_now=True) 136 | 137 | objects = AccountManager() 138 | 139 | USERNAME_FIELD = 'email' 140 | REQUIRED_FIELDS = ['username'] 141 | 142 | class Meta: 143 | ordering = ['slug'] 144 | 145 | def save(self, *args, **kwargs): 146 | 147 | slug = slugify(self.username) 148 | self.slug = slug 149 | count = 0 150 | while Account.objects.filter(slug=self.slug).exclude(pk=self.id).exists(): 151 | self.slug = '{0}{1}'.format(slug, count) 152 | logger.debug('Slug conflict. Trying again with {0}'.format(self.slug)) 153 | count += 1 154 | 155 | super(Account, self).save(*args, **kwargs) 156 | 157 | def __str__(self): 158 | return '@{0}'.format(self.username) 159 | 160 | def get_full_name(self): 161 | return self.name 162 | 163 | def get_short_name(self): 164 | if (self.name != ''): 165 | # Try to extract the first name 166 | names = self.name.split() 167 | first_name = names[0] 168 | return first_name 169 | return self.username 170 | 171 | # For full access to Permission system, we needed to add the PermissionMixin 172 | def has_perm(self, perm, obj=None): 173 | return True 174 | 175 | def has_perms(perm_list, obj=None): 176 | return True 177 | 178 | def has_module_perms(self, app_label): 179 | return True 180 | 181 | # Custom Methods 182 | # -------------- 183 | def get_absolute_url(self): 184 | return '/account/%s/' % self.slug 185 | 186 | def get_edit_url(self): 187 | return '%sedit/' % self.get_absolute_url() 188 | 189 | def get_token(self): 190 | try: 191 | token = Token.objects.get(user=self) 192 | except: 193 | token = Token.objects.create(user=self) 194 | return token 195 | 196 | def get_gravatar_thumbnail_url(self, size=100): 197 | # Set your variables here 198 | email = self.email 199 | default = 'identicon' 200 | 201 | # construct the url 202 | gravatar_url = "https://secure.gravatar.com/avatar/" + hashlib.md5(email.lower().encode('utf-8')).hexdigest() + "?" 203 | try: 204 | # Python 3.4 205 | gravatar_url += urllib.parse.urlencode({'d': default, 's': str(size)}) 206 | except: 207 | # Python 2.7 208 | gravatar_url += urllib.urlencode({'d': default, 's': str(size)}) 209 | 210 | return gravatar_url 211 | -------------------------------------------------------------------------------- /server/apps/authentication/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | 3 | 4 | class IsAccountOwner(permissions.BasePermission): 5 | def has_object_permission(self, request, view, account): 6 | if request.user: 7 | return account == request.user 8 | return False 9 | -------------------------------------------------------------------------------- /server/apps/authentication/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import update_session_auth_hash 2 | 3 | from rest_framework import serializers 4 | from rest_framework.authtoken.models import Token 5 | 6 | from .models import Account 7 | 8 | 9 | class AccountSerializer(serializers.ModelSerializer): 10 | password = serializers.CharField(write_only=True, required=False) 11 | confirm_password = serializers.CharField(write_only=True, required=False) 12 | 13 | class Meta: 14 | model = Account 15 | fields = ('id', 'email', 'username', 'created_at', 'updated_at', 16 | 'name', 'tagline', 'password', 17 | 'confirm_password',) 18 | read_only_fields = ('created_at', 'updated_at',) 19 | 20 | def create(self, validated_data): 21 | return Account.objects.create(**validated_data) 22 | 23 | def update(self, instance, validated_data): 24 | instance.username = validated_data.get('username', instance.username) 25 | instance.tagline = validated_data.get('tagline', instance.tagline) 26 | 27 | instance.save() 28 | 29 | password = validated_data.get('password', None) 30 | confirm_password = validated_data.get('confirm_password', None) 31 | 32 | if password and confirm_password and password == confirm_password: 33 | instance.set_password(password) 34 | instance.is_active = True 35 | instance.save() 36 | 37 | update_session_auth_hash(self.context.get('request'), instance) 38 | 39 | return instance 40 | 41 | def to_representation(self, obj): 42 | data = super(AccountSerializer, self).to_representation(obj) 43 | 44 | return data 45 | 46 | 47 | class LoginCustomSerializer(serializers.Serializer): 48 | email = serializers.EmailField(max_length=200) 49 | password = serializers.CharField(max_length=200) 50 | 51 | 52 | class PasswordCustomSerializer(serializers.Serializer): 53 | password = serializers.CharField(max_length=200) 54 | 55 | -------------------------------------------------------------------------------- /server/apps/authentication/tasks.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | 4 | from django.conf import settings 5 | from django.template.defaultfilters import slugify 6 | 7 | # Get an instance of a logger 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | def send_new_user_notification(id, username, email): 12 | ''' 13 | Information about the ContactMe form is sent via 14 | SQS to an EB worker, who will then send notifications 15 | to our Staff 16 | 17 | :param instance: ContactMessage object to email 18 | :return: Nothing 19 | ''' 20 | 21 | subject = 'User @{0} (ID={1}) has registered with email {2}'.format(username, id, email) 22 | 23 | logger.debug(subject) 24 | -------------------------------------------------------------------------------- /server/apps/authentication/templates/authentication/detailVCard.html: -------------------------------------------------------------------------------- 1 | {% templatetag openblock %} extends "base.html" {% templatetag closeblock %} 2 | {% templatetag openblock %} load static {% templatetag closeblock %} 3 | {% templatetag openblock %} load i18n {% templatetag closeblock %} 4 | 5 | {% templatetag openblock %} block content {% templatetag closeblock %} 6 | 7 | 8 | {% templatetag openblock %} endblock {% templatetag closeblock %} 9 | 10 | -------------------------------------------------------------------------------- /server/apps/authentication/tests.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytz 4 | 5 | from django.conf import settings 6 | from django.contrib.auth import get_user_model 7 | from django.test import Client, TestCase 8 | from django.utils import timezone 9 | 10 | from rest_framework import status 11 | from rest_framework.authtoken.models import Token 12 | from rest_framework.reverse import reverse 13 | from rest_framework.test import APIClient, APIRequestFactory 14 | 15 | from .models import * 16 | from .serializers import AccountSerializer 17 | from .tasks import * 18 | 19 | user_model = get_user_model() 20 | 21 | class MainTestCase(TestCase): 22 | """ 23 | Fixure includes: 24 | """ 25 | #fixtures = ['testdb_main.json'] 26 | 27 | def setUp(self): 28 | self.u1 = user_model.objects.create_superuser(username='user1', email='user1@foo.com', password='pass') 29 | self.u1.name = 'User One' 30 | self.u1.is_active = True 31 | self.u1.save() 32 | self.u2 = user_model.objects.create_user(username='user2', email='user2@foo.com', password='pass') 33 | self.u2.name = 'User G Two' 34 | self.u2.time_zone = 'America/Chicago' 35 | self.u2.save() 36 | self.u3 = user_model.objects.create_user(username='user3', email='user3@foo.com', password='pass') 37 | self.assertEqual(user_model.objects.count(), 3) 38 | self.token1 = Token.objects.create(user=self.u1) 39 | self.token2 = Token.objects.create(user=self.u2) 40 | 41 | def tearDown(self): 42 | user_model.objects.all().delete() 43 | Token.objects.all().delete() 44 | 45 | def test_full_short_names(self): 46 | self.assertEqual(self.u3.get_full_name(), u'') 47 | self.assertEqual(self.u3.get_short_name(), self.u3.username) 48 | self.assertEqual(self.u2.get_full_name(), self.u2.name) 49 | self.assertEqual(self.u2.get_short_name(), u'User') 50 | 51 | def test_basic_gets(self): 52 | ok = self.client.login(email='user1@foo.com', password='pass') 53 | 54 | profile_url = reverse('account_detail', kwargs={'slug':'user1'}) 55 | resp = self.client.get(profile_url, format='json') 56 | self.assertEqual(resp.status_code, status.HTTP_200_OK) 57 | 58 | # account should also redirect to the above 59 | url = reverse('account_redirect') 60 | resp = self.client.get(url, format='json') 61 | self.assertRedirects(resp, expected_url=profile_url, status_code=302, target_status_code=200) 62 | 63 | url = reverse('account_edit', kwargs={'slug':'user1'}) 64 | resp = self.client.get(url, format='json') 65 | self.assertEqual(resp.status_code, status.HTTP_200_OK) 66 | 67 | # TODO: What's the URL namespace for django-allauth 68 | # TODO: Enable test when a social login is added 69 | ''' 70 | url = '/account/email/' 71 | resp = self.client.get(url, format='json') 72 | self.assertEqual(resp.status_code, status.HTTP_200_OK) 73 | url = '/account/social/connections/' 74 | resp = self.client.get(url, format='json') 75 | self.assertEqual(resp.status_code, status.HTTP_200_OK) 76 | url = '/account/password/change/' 77 | resp = self.client.get(url, format='json') 78 | self.assertEqual(resp.status_code, status.HTTP_200_OK) 79 | ''' 80 | 81 | 82 | def test_no_slug_conflicts(self): 83 | u1 = user_model.objects.create_user(username='User.4', email='user41@foo.com', password='pass') 84 | self.assertEqual(u1.slug, 'user4') 85 | u2 = user_model.objects.create_user(username='User4', email='user42@foo.com', password='pass') 86 | # Because user4 is taken, it will add a digit until it finds a match 87 | self.assertEqual(u2.slug, 'user40') 88 | 89 | def test_api_token(self): 90 | 91 | url = reverse('api-token') 92 | u4 = user_model.objects.create_user(username='User4', email='user4@foo.com', password='pass') 93 | u4.is_active = True 94 | u4.save() 95 | 96 | try: 97 | token = Token.objects.get(user=u4) 98 | # There should be no token for u3 99 | self.assertEqual(1, 0) 100 | except: 101 | pass 102 | 103 | resp = self.client.get(url, data={'format': 'json'}) 104 | self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN) 105 | ok = self.client.login(email='user4@foo.com', password='pass') 106 | self.assertTrue(ok) 107 | resp = self.client.get(url, data={'format': 'json'}) 108 | self.assertEqual(resp.status_code, status.HTTP_201_CREATED) 109 | deserialized = json.loads(resp.content.decode()) 110 | self.assertEqual(len(deserialized), 1) 111 | token = Token.objects.get(user=u4) 112 | self.assertEqual(deserialized['token'], token.key) 113 | resp = self.client.get(url, data={'format': 'json'}) 114 | self.assertEqual(resp.status_code, status.HTTP_200_OK) 115 | 116 | def test_serializer(self): 117 | account = Account.objects.latest('created_at') 118 | serialized_account = AccountSerializer(account) 119 | email = serialized_account.data.get('email') 120 | username = serialized_account.data.get('username') 121 | self.assertEqual(username, 'user3') 122 | self.assertEqual(email, 'user3@foo.com') 123 | 124 | def testTimeZone(self): 125 | #tzname = request.session.get('django_timezone') 126 | response = self.client.get('/') 127 | self.assertEqual(response.status_code, status.HTTP_200_OK) 128 | 129 | # If not loggedin, no timezone in session 130 | session = self.client.session 131 | self.assertFalse('django_timezone' in session) 132 | 133 | ok = self.client.login(email='user1@foo.com', password='pass') 134 | self.assertTrue(ok) 135 | response = self.client.get('/') 136 | self.assertEqual(response.status_code, status.HTTP_200_OK) 137 | 138 | # Default Time zone 139 | session = self.client.session 140 | self.assertTrue('django_timezone' in session) 141 | self.assertEqual(session["django_timezone"], timezone.get_default_timezone_name()) 142 | 143 | self.client.logout() 144 | 145 | u4 = user_model.objects.create_user(username='user4', email='user4@foo.com', password='pass') 146 | u4.name = 'New York Dude' 147 | u4.time_zone = 'America/New_York' 148 | u4.is_active = True 149 | u4.save() 150 | 151 | ok = self.client.login(email='user4@foo.com', password='pass') 152 | self.assertTrue(ok) 153 | response = self.client.get('/') 154 | self.assertEqual(response.status_code, status.HTTP_200_OK) 155 | 156 | # Default Time zone 157 | session = self.client.session 158 | self.assertTrue('django_timezone' in session) 159 | self.assertEqual(session["django_timezone"], timezone.get_current_timezone_name()) 160 | self.assertEqual(timezone.get_current_timezone_name(), 'America/New_York') 161 | 162 | self.client.logout() 163 | 164 | 165 | def test_registration_notification(self): 166 | send_new_user_notification(self.u1.id, self.u1.username, self.u1.email) 167 | 168 | from rest_framework.test import APITestCase 169 | 170 | 171 | class AccountAPITests(APITestCase): 172 | 173 | def setUp(self): 174 | self.u1 = user_model.objects.create_superuser(username='user1', email='user1@foo.com', password='pass') 175 | self.u1.is_active = True; 176 | self.u1.name = 'User One' 177 | self.u1.save() 178 | self.u2 = user_model.objects.create_user(username='user2', email='user2@foo.com', password='pass') 179 | self.u3 = user_model.objects.create_user(username='user3', email='user3@foo.com', password='pass') 180 | self.token1 = Token.objects.create(user=self.u1) 181 | self.token2 = Token.objects.create(user=self.u2) 182 | 183 | def tearDown(self): 184 | user_model.objects.all().delete() 185 | Token.objects.all().delete() 186 | 187 | def test_create_account(self): 188 | """ 189 | Ensure we can create a new account object. 190 | """ 191 | url = reverse('account-list') 192 | data = {'username':'user5', 193 | 'email':'user5@foo.com', 194 | 'password':'pass', 195 | 'confirm_password':'pass'} 196 | response = self.client.post(url, data, format='json') 197 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 198 | self.assertEqual(response.data, data) 199 | 200 | ok = self.client.login(email='user5@foo.com', password='pass') 201 | self.assertTrue(ok) 202 | 203 | response = self.client.get(url, data={'format': 'json'}) 204 | self.assertEqual(response.status_code, status.HTTP_200_OK) 205 | deserialized = json.loads((response.content).decode()) 206 | self.assertEqual(deserialized['count'], 4) 207 | self.client.logout() 208 | 209 | def test_GET_Account(self): 210 | 211 | ok = self.client.login(email='user1@foo.com', password='pass') 212 | self.assertTrue(ok) 213 | url = reverse('account-detail', kwargs={'username':'user1'}) 214 | resp = self.client.get(url, data={'format': 'json'}) 215 | self.assertEqual(resp.status_code, status.HTTP_200_OK) 216 | 217 | self.assertEqual(resp.data['id'], 1) 218 | self.assertEqual(resp.data['username'], u'user1') 219 | self.assertEqual(resp.data['email'], u'user1@foo.com') 220 | self.assertFalse('token' in resp.data) 221 | self.client.logout() 222 | 223 | def test_PATCH_Account(self): 224 | 225 | url = reverse('account-detail', kwargs={'username':'user1'}) 226 | 227 | ok = self.client.login(email='user1@foo.com', password='pass') 228 | self.assertTrue(ok) 229 | 230 | resp = self.client.get(url, data={'format': 'json'}) 231 | self.assertEqual(resp.status_code, status.HTTP_200_OK) 232 | self.assertEqual(resp.data['id'], 1) 233 | self.assertEqual(resp.data['username'], u'user1') 234 | self.assertEqual(resp.data['email'], u'user1@foo.com') 235 | self.assertEqual(resp.data['name'], u'User One') 236 | self.assertEqual(resp.data['tagline'], u'') 237 | 238 | new_tagline = 'Awesome' 239 | data = {'tagline':new_tagline} 240 | 241 | resp = self.client.patch(url, data=data, format='json') 242 | 243 | self.assertEqual(resp.status_code, status.HTTP_200_OK) 244 | 245 | resp = self.client.get(url, data={'format': 'json'}) 246 | self.assertEqual(resp.status_code, status.HTTP_200_OK) 247 | self.assertEqual(resp.data['tagline'], new_tagline) 248 | 249 | def test_GET_Accounts(self): 250 | 251 | ok = self.client.login(email='user1@foo.com', password='pass') 252 | self.assertTrue(ok) 253 | url = reverse('account-list') 254 | resp = self.client.get(url, data={'format': 'json'}) 255 | self.assertEqual(resp.status_code, status.HTTP_200_OK) 256 | deserialized = json.loads(resp.content.decode()) 257 | self.assertEqual(deserialized['count'], 3) 258 | 259 | self.assertEqual([obj['id'] for obj in deserialized['results']], [1,2,3]) 260 | self.assertEqual([obj['username'] for obj in deserialized['results']], [u'user1', u'user2', u'user3']) 261 | self.assertEqual([obj['email'] for obj in deserialized['results']], [u'user1@foo.com', 262 | u'user2@foo.com', 263 | u'user3@foo.com']) 264 | self.assertFalse('token' in deserialized['results'][0]) 265 | self.client.logout() 266 | 267 | def test_basic_POST_Account(self): 268 | 269 | 270 | url = reverse('account-list') 271 | resp = self.client.post(url, {'username':'user4', 272 | 'email':'user4@foo.com', 273 | 'password':'pass', 274 | 'confirm_password':'pass'}, format='json') 275 | self.assertEqual(resp.status_code, status.HTTP_201_CREATED) 276 | u4 = user_model.objects.get(slug='user4') 277 | u4.is_active = True 278 | u4.save() 279 | ok = self.client.login(email='user4@foo.com', password='pass') 280 | self.assertTrue(ok) 281 | 282 | resp = self.client.get(url, data={'format': 'json'}) 283 | self.assertEqual(resp.status_code, status.HTTP_200_OK) 284 | deserialized = json.loads(resp.content.decode()) 285 | self.assertEqual(deserialized['count'], 4) 286 | self.client.logout() 287 | 288 | # No duplicates 289 | data = {'username':'user4', 290 | 'email':'user4@foo.com', 291 | 'password':'pass', 292 | 'confirm_password':'pass'} 293 | 294 | resp = self.client.post(url, data=data, format='json') 295 | self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) 296 | 297 | data['username'] = 'user5' 298 | resp = self.client.post(url, data=data, format='json') 299 | self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) 300 | 301 | data['username'] = 'user4' 302 | data['email'] = 'user5@foo.com' 303 | resp = self.client.post(url, data=data, format='json') 304 | self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) 305 | 306 | data['username'] = 'user5' 307 | resp = self.client.post(url, data=data, format='json') 308 | self.assertEqual(resp.status_code, status.HTTP_201_CREATED) 309 | 310 | data['username'] = 'user6' 311 | data['email'] = 'user6@foo.com' 312 | data['confirm_password'] = 'pass1' 313 | resp = self.client.post(url, data=data, format='json') 314 | self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) 315 | 316 | data['tagline'] = 'Awesome' 317 | data['name'] = 'User One' 318 | data['confirm_password'] = 'pass' 319 | resp = self.client.post(url, data=data, format='json') 320 | self.assertEqual(resp.status_code, status.HTTP_201_CREATED) 321 | 322 | def test_login_api(self): 323 | 324 | client = self.client 325 | 326 | url1 = reverse('api-token-auth') 327 | 328 | # This is the django rest framework provided function 329 | resp = client.post(url1, {'username':'user1@foo.com', 'password':'pass'}, format='json') 330 | self.assertEqual(resp.status_code, status.HTTP_200_OK) 331 | self.assertTrue('token' in resp.data) 332 | self.assertEqual(resp.data['token'], self.token1.key) 333 | 334 | client.logout() 335 | 336 | url2 = reverse('api-login') 337 | resp = client.post(url2, {'email':'user1@foo.com', 'password':'pass'}, format='json') 338 | self.assertEqual(resp.status_code, status.HTTP_200_OK) 339 | self.assertTrue('token' in resp.data) 340 | self.assertEqual(resp.data['token'], self.token1.key) 341 | 342 | client.logout() 343 | 344 | resp = client.post(url1, {'username':'user1@foo.com'}, format='json') 345 | self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) 346 | 347 | resp = client.post(url1, {'username':'user101@foo.com', 'password':'pass'}, format='json') 348 | self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) 349 | 350 | resp = client.post(url2, {'email':'user1@foo.com'}, format='json') 351 | self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) 352 | 353 | resp = client.post(url2, {'email':'user101@foo.com', 'password':'pass'}, format='json') 354 | self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED) 355 | 356 | # Test that we cannot login if not Active 357 | u5 = user_model.objects.create_user(username='user5', email='user5@foo.com', password='pass') 358 | u5.is_active=False 359 | u5.save() 360 | ok = client.login(email='user5@foo.com', password='pass') 361 | self.assertFalse(ok) 362 | resp = client.post(url1, {'username':'user5@foo.com', 'password':'pass'}, format='json') 363 | self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) 364 | resp = client.post(url2, {'email':'user5@foo.com', 'password':'pass'}, format='json') 365 | self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED) 366 | 367 | def test_user_info_api(self): 368 | 369 | url = reverse('api-user-info') 370 | 371 | resp = self.client.get(url, data={'format': 'json'}) 372 | self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN) 373 | 374 | ok = self.client.login(email='user1@foo.com', password='pass') 375 | self.assertTrue(ok) 376 | 377 | resp = self.client.get(url, data={'format': 'json'}) 378 | self.assertEqual(resp.status_code, status.HTTP_200_OK) 379 | 380 | self.assertEqual(resp.data['tagline'], '') 381 | self.assertEqual(resp.data['name'], 'User One') 382 | self.assertEqual(resp.data['username'], 'user1') 383 | self.assertEqual(resp.data['email'], 'user1@foo.com') 384 | 385 | def test_PUT_Account(self): 386 | 387 | url = reverse('account-detail', kwargs={'username':'user1'}) 388 | 389 | ok = self.client.login(email='user1@foo.com', password='pass') 390 | self.assertTrue(ok) 391 | 392 | resp = self.client.get(url, data={'format': 'json'}) 393 | self.assertEqual(resp.status_code, status.HTTP_200_OK) 394 | self.assertEqual(resp.data['id'], 1) 395 | self.assertEqual(resp.data['username'], u'user1') 396 | self.assertEqual(resp.data['email'], u'user1@foo.com') 397 | self.assertEqual(resp.data['name'], u'User One') 398 | self.assertEqual(resp.data['tagline'], u'') 399 | 400 | new_tagline = 'Awesome' 401 | data = {'email':self.u1.email, 402 | 'username':self.u1.username, 403 | 'name':'User One', 404 | 'tagline':new_tagline} 405 | 406 | resp = self.client.put(url, data=data, format='json') 407 | 408 | self.assertEqual(resp.status_code, status.HTTP_200_OK) 409 | 410 | resp = self.client.get(url, data={'format': 'json'}) 411 | self.assertEqual(resp.status_code, status.HTTP_200_OK) 412 | self.assertEqual(resp.data['tagline'], data['tagline']) 413 | self.assertEqual(resp.data['username'], data['username']) 414 | self.assertEqual(resp.data['name'], data['name']) 415 | self.assertEqual(resp.data['email'], data['email']) 416 | 417 | def test_logout_api(self): 418 | 419 | url = reverse('api-login') 420 | client = self.client 421 | 422 | resp = client.post(url, {'email':'user1@foo.com', 'password':'pass'}, format='json') 423 | self.assertEqual(resp.status_code, status.HTTP_200_OK) 424 | self.assertTrue('token' in resp.data) 425 | self.assertEqual(resp.data['token'], self.token1.key) 426 | 427 | url = reverse('api-logout') 428 | resp = client.post(url, format='json') 429 | self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT) 430 | 431 | def test_set_password_api(self): 432 | 433 | url = '/api/v1/account/user2/set_password' 434 | client = self.client 435 | ok = self.u2.check_password('pass') 436 | self.assertTrue(ok) 437 | 438 | resp = client.post(url, {'password':'pass2'}, format='json') 439 | self.assertEqual(resp.status_code, status.HTTP_201_CREATED) 440 | self.u2 = Account.objects.get(pk=2) 441 | ok = self.u2.check_password('pass') 442 | self.assertFalse(ok) 443 | ok = self.u2.check_password('pass2') 444 | self.assertTrue(ok) 445 | 446 | client.logout() 447 | 448 | -------------------------------------------------------------------------------- /server/apps/authentication/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | 3 | from .views import AccountDetailView, AccountRedirectView, AccountUpdateView 4 | 5 | urlpatterns = [ 6 | 7 | path('', include('allauth.urls')), 8 | path('', AccountRedirectView.as_view(), name='account_redirect'), 9 | path('/edit/', AccountUpdateView.as_view(), name='account_edit'), 10 | path('/', AccountDetailView.as_view(), name='account_detail'), 11 | ] 12 | -------------------------------------------------------------------------------- /server/apps/authentication/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | 5 | from django.conf import settings 6 | from django.contrib.auth.decorators import login_required 7 | from django.http import HttpResponse, HttpResponseForbidden, HttpResponseRedirect 8 | from django.shortcuts import get_object_or_404 9 | from django.urls import reverse 10 | from django.utils.decorators import method_decorator 11 | from django.views.decorators.csrf import csrf_exempt 12 | from django.views.generic import CreateView, DetailView, RedirectView, TemplateView, View 13 | from django.views.generic.edit import FormView, UpdateView 14 | 15 | from .forms import * 16 | from .models import Account 17 | 18 | # Get an instance of a logger 19 | logger = logging.getLogger(__name__) 20 | 21 | 22 | class AccountRedirectView(View): 23 | @method_decorator(login_required) 24 | def get(self, request): 25 | user = request.user 26 | 27 | return HttpResponseRedirect(reverse('account_detail', args=(user.username,))) 28 | 29 | 30 | class AccountDetailView(DetailView): 31 | model = Account 32 | template_name = 'authentication/detailVCard.html' 33 | 34 | def get_context_data(self, **kwargs): 35 | context = super(AccountDetailView, self).get_context_data(**kwargs) 36 | context['referer'] = self.request.META.get('HTTP_REFERER') 37 | context['is_owner'] = (self.object == self.request.user) 38 | return context 39 | 40 | @method_decorator(login_required) 41 | def dispatch(self, request, *args, **kwargs): 42 | return super(AccountDetailView, self).dispatch(request, *args, **kwargs) 43 | 44 | 45 | class AccountThumbnailView(RedirectView): 46 | 47 | permanent = False 48 | query_string = True 49 | 50 | def get_redirect_url(self, *args, **kwargs): 51 | user = get_object_or_404(Account, slug=kwargs['slug']) 52 | return user.get_thumbnail_url() 53 | 54 | 55 | class AccountTinyView(RedirectView): 56 | 57 | permanent = False 58 | query_string = True 59 | 60 | def get_redirect_url(self, *args, **kwargs): 61 | user = get_object_or_404(Account, slug=kwargs['slug']) 62 | return user.get_tiny_url() 63 | 64 | 65 | class AccountUpdateView(UpdateView): 66 | model = Account 67 | form_class = AccountUpdateForm 68 | template_name = 'form.html' 69 | 70 | def form_valid(self, form): 71 | self.object = form.save(commit=False) 72 | self.object.time_zone = form.cleaned_data.get('time_zone') 73 | self.object.save() 74 | 75 | # Update session as well 76 | self.request.session['django_timezone'] = str(form.cleaned_data.get('time_zone')) 77 | 78 | return HttpResponseRedirect(self.get_success_url()) 79 | 80 | def get_context_data(self, **kwargs): 81 | context = super(AccountUpdateView, self).get_context_data(**kwargs) 82 | context['referer'] = self.request.META.get('HTTP_REFERER') 83 | 84 | return context 85 | 86 | @method_decorator(login_required) 87 | def dispatch(self, request, *args, **kwargs): 88 | return super(AccountUpdateView, self).dispatch(request, *args, **kwargs) 89 | 90 | 91 | -------------------------------------------------------------------------------- /server/apps/main/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkarchmer/django-aws-template/2bf15740cdad0ff9e6656c3c452a5ee6042cc0a6/server/apps/main/__init__.py -------------------------------------------------------------------------------- /server/apps/main/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import * 4 | 5 | 6 | class ContactMessageAdmin(admin.ModelAdmin): 7 | list_display = ('id', 'name', 'email') 8 | 9 | 10 | """ 11 | Register Admin Pages 12 | """ 13 | admin.site.register(ContactMessage, ContactMessageAdmin) 14 | -------------------------------------------------------------------------------- /server/apps/main/api_views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.conf import settings 4 | from django.contrib.auth.models import User 5 | from django.core.exceptions import PermissionDenied 6 | from django.http import Http404, HttpResponse 7 | from django.shortcuts import get_object_or_404 8 | 9 | from rest_framework import generics, mixins, status, viewsets 10 | from rest_framework.parsers import JSONParser 11 | from rest_framework.permissions import IsAdminUser 12 | from rest_framework.renderers import JSONRenderer 13 | from rest_framework.response import Response 14 | from rest_framework.views import APIView 15 | 16 | from .models import ContactMessage 17 | from .permissions import ContactMessagePermission 18 | from .serializers import ContactMessageSerializer 19 | from .tasks import send_contact_me_notification 20 | 21 | 22 | class APIMessageViewSet(mixins.CreateModelMixin, 23 | mixins.ListModelMixin, 24 | mixins.RetrieveModelMixin, 25 | viewsets.GenericViewSet): 26 | """ 27 | This viewset automatically provides `list`, `create`, `retrieve`, 28 | `update` and `destroy` actions. 29 | 30 | Additionally we also provide an extra `highlight` action. 31 | """ 32 | queryset = ContactMessage.objects.all() 33 | serializer_class = ContactMessageSerializer 34 | permission_classes = (ContactMessagePermission,) 35 | 36 | 37 | def get_queryset(self): 38 | """ 39 | This view should return a list of all records 40 | for the currently authenticated user. 41 | """ 42 | #user = self.request.user 43 | return ContactMessage.objects.all() 44 | 45 | def perform_create(self, serializer): 46 | # Include the owner attribute directly, rather than from request data. 47 | instance = serializer.save() 48 | 49 | # Schedule a task to send email 50 | send_contact_me_notification(instance) 51 | 52 | -------------------------------------------------------------------------------- /server/apps/main/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MainConfig(AppConfig): 5 | name = 'apps.main' -------------------------------------------------------------------------------- /server/apps/main/forms.py: -------------------------------------------------------------------------------- 1 | from captcha.fields import ReCaptchaField 2 | 3 | from django import forms as forms 4 | from django.forms import ModelForm 5 | 6 | from .models import ContactMessage 7 | 8 | 9 | class LoginForm(forms.Form): 10 | username = forms.CharField(max_length=100) 11 | password = forms.CharField(widget=forms.PasswordInput) 12 | 13 | 14 | class RegistrationForm(forms.Form): 15 | email = forms.EmailField(widget=forms.TextInput(attrs={'size':'30'})) 16 | username = forms.CharField(widget=forms.TextInput(attrs={'size':'20'})) 17 | password1 = forms.CharField(widget=forms.PasswordInput) 18 | password2 = forms.CharField(widget=forms.PasswordInput) 19 | 20 | 21 | class ContactPublicForm(ModelForm): 22 | captcha = ReCaptchaField() 23 | class Meta: 24 | model = ContactMessage 25 | exclude = ['phone'] 26 | 27 | def __init__(self, *args, **kwargs): 28 | super(ContactPublicForm, self).__init__(*args, **kwargs) 29 | self.fields['message'].widget.attrs['class'] = 'custom-form-element custom-textarea' 30 | self.fields['message'].widget.attrs['size'] = '30' 31 | self.fields['name'].widget.attrs['class'] = 'custom-form-element custom-input-text' 32 | self.fields['email'].widget.attrs['class'] = 'custom-form-element custom-input-text' 33 | 34 | 35 | class ContactPrivateForm(ModelForm): 36 | class Meta: 37 | model = ContactMessage 38 | exclude = ['name', 'phone', 'email'] 39 | 40 | def __init__(self, *args, **kwargs): 41 | super(ContactPrivateForm, self).__init__(*args, **kwargs) 42 | self.fields['message'].widget.attrs['class'] = 'custom-form-element custom-textarea' 43 | self.fields['message'].widget.attrs['size'] = '30' 44 | 45 | -------------------------------------------------------------------------------- /server/apps/main/management/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'dkarchmer' 2 | -------------------------------------------------------------------------------- /server/apps/main/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'dkarchmer' 2 | -------------------------------------------------------------------------------- /server/apps/main/management/commands/clear-cache.py: -------------------------------------------------------------------------------- 1 | from django.core.cache import cache 2 | from django.core.management.base import BaseCommand 3 | 4 | 5 | class Command(BaseCommand): 6 | def handle(self, *args, **kwargs): 7 | cache.clear() 8 | self.stdout.write('Cleared cache\n') -------------------------------------------------------------------------------- /server/apps/main/management/commands/init-basic-data.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.sites.models import Site 3 | from django.contrib.sites.shortcuts import get_current_site 4 | from django.core.management.base import BaseCommand 5 | 6 | from allauth.account.models import EmailAddress 7 | from allauth.socialaccount.models import SocialApp 8 | 9 | from apps.authentication.models import Account 10 | 11 | 12 | class Command(BaseCommand): 13 | 14 | def _create_super_users(self): 15 | for user in getattr(settings, 'ADMINS'): 16 | username = user[0].replace(' ', '') 17 | email = user[1] 18 | password = 'admin' 19 | print('Creating account for %s (%s)' % (username, email)) 20 | admin = Account.objects.create_superuser(email=email, username=username, password=password) 21 | admin.is_active = True 22 | admin.is_admin = True 23 | admin.save() 24 | EmailAddress.objects.create(email=email, user=admin, verified=True, primary=True) 25 | 26 | def _create_social_accounts(self, site): 27 | # For test/stage, also create dummy Facebook setup 28 | print('Creating Dummy SocialApp for Facebook') 29 | app = SocialApp.objects.create(provider='facebook', 30 | name='Facebook', 31 | client_id='Foo', 32 | secret='Bar' 33 | ) 34 | app.sites.add(site) 35 | app.save() 36 | app = SocialApp.objects.create(provider='twitter', 37 | name='Twitter', 38 | client_id='Foo', 39 | secret='Bar' 40 | ) 41 | app.sites.add(site) 42 | app.save() 43 | app = SocialApp.objects.create(provider='google', 44 | name='Google', 45 | client_id='Foo', 46 | secret='Bar' 47 | ) 48 | app.sites.add(site) 49 | app.save() 50 | 51 | def handle(self, *args, **options): 52 | 53 | if Account.objects.count() == 0: 54 | # If there are no Accounts, we can assume this is a new Env 55 | # create a super user 56 | self._create_super_users() 57 | 58 | if SocialApp.objects.count() == 0: 59 | # Also fixup the Site info 60 | site = Site.objects.get_current() 61 | site.domain_name = getattr(settings, 'DOMAIN_NAME') 62 | site.display_name = getattr(settings, 'COMPANY_NAME') 63 | site.save() 64 | if not getattr(settings, 'PRODUCTION'): 65 | self._create_social_accounts(site) 66 | 67 | else: 68 | print('Admin accounts can only be initialized if no Accounts exist') 69 | -------------------------------------------------------------------------------- /server/apps/main/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.4 on 2016-03-16 23:23 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='ContactMessage', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('name', models.CharField(max_length=50)), 21 | ('email', models.EmailField(max_length=60)), 22 | ('phone', models.CharField(blank=True, max_length=50)), 23 | ('message', models.TextField()), 24 | ('created_on', models.DateTimeField(auto_now_add=True, verbose_name='created_on')), 25 | ], 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /server/apps/main/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkarchmer/django-aws-template/2bf15740cdad0ff9e6656c3c452a5ee6042cc0a6/server/apps/main/migrations/__init__.py -------------------------------------------------------------------------------- /server/apps/main/models.py: -------------------------------------------------------------------------------- 1 | import mimetypes 2 | import os 3 | 4 | from django.db import models 5 | 6 | 7 | class ContactMessage(models.Model): 8 | 9 | name = models.CharField(max_length=50) 10 | email = models.EmailField(max_length=60) 11 | phone = models.CharField(max_length=50, blank=True) 12 | message = models.TextField() 13 | 14 | created_on = models.DateTimeField('created_on', auto_now_add=True) 15 | 16 | class Meta: 17 | ordering = ['id'] 18 | 19 | def __str__(self): 20 | return self.name 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /server/apps/main/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | 3 | 4 | class ContactMessagePermission(permissions.BasePermission): 5 | """ 6 | Custom permission to only allow owners of an object to access (read/write) 7 | """ 8 | 9 | def has_permission(self, request, view): 10 | if request.method == 'POST': 11 | return True 12 | else: 13 | return request.user.is_staff 14 | 15 | def has_object_permission(self, request, view, obj): 16 | # Everybody can submit 17 | return request.user.is_staff 18 | -------------------------------------------------------------------------------- /server/apps/main/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from .models import ContactMessage 4 | 5 | 6 | class ContactMessageSerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = ContactMessage 9 | fields = ('id', 'name', 'email', 'phone', 'message', 'created_on') 10 | read_only_fields = ('created_on',) 11 | 12 | -------------------------------------------------------------------------------- /server/apps/main/tasks.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import logging 4 | 5 | from django.core.mail import mail_admins 6 | 7 | # Get an instance of a logger 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | def send_contact_me_notification(instance): 12 | ''' 13 | Use SNS to notify Staff of person trying to contact 14 | us via the Landing Page 15 | 16 | :param instance: ContactMessage object to email 17 | :return: Nothing 18 | ''' 19 | 20 | message = '==========================\n' 21 | message += '%s\n' % instance.name 22 | message += '%s\n' % instance.email 23 | message += '==========================\n' 24 | message += instance.message 25 | message += '\n' 26 | message += '==========================\n' 27 | 28 | mail_admins(subject='New Message', message=message) 29 | 30 | return True 31 | 32 | -------------------------------------------------------------------------------- /server/apps/main/templates/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% for domain in extra_domains %} 5 | 6 | {% endfor %} 7 | 8 | -------------------------------------------------------------------------------- /server/apps/main/templates/main/about.html: -------------------------------------------------------------------------------- 1 | {% templatetag openblock %} extends "base.html" {% templatetag closeblock %} 2 | {% templatetag openblock %} load static {% templatetag closeblock %} 3 | {% templatetag openblock %} load i18n {% templatetag closeblock %} 4 | 5 | {% templatetag openblock %} block content {% templatetag closeblock %} 6 | 7 |
8 |

About this site

9 |

Hope this template helps you out

10 |
11 | 12 |
13 |
14 |

Template generated with django-aws-template

15 |

16 |
17 | 18 | 21 | 22 | {% templatetag openblock %} endblock content {% templatetag closeblock %} 23 | -------------------------------------------------------------------------------- /server/apps/main/templates/main/index.html: -------------------------------------------------------------------------------- 1 | {% templatetag openblock %} extends "base.html" {% templatetag closeblock %} 2 | {% templatetag openblock %} load static {% templatetag closeblock %} 3 | {% templatetag openblock %} load i18n {% templatetag closeblock %} 4 | 5 | {% templatetag openblock %} block container {% templatetag closeblock %} 6 |
7 |
8 | 13 |

{{ project_name }}

14 |
15 | 16 |
17 |

Hello @{{ user.username }}!

18 |

Hope this template helps you out

19 |

Splendid!

20 |
21 | 22 |
23 |
24 |

Template generated with django-aws-template

25 |

26 |
27 | 28 | 31 |
32 | {% templatetag openblock %} endblock container {% templatetag closeblock %} 33 | 34 | -------------------------------------------------------------------------------- /server/apps/main/templates/main/landing.html: -------------------------------------------------------------------------------- 1 | {% templatetag openblock %} extends "base.html" {% templatetag closeblock %} 2 | {% templatetag openblock %} load static {% templatetag closeblock %} 3 | {% templatetag openblock %} load i18n {% templatetag closeblock %} 4 | 5 | {% templatetag openblock %} block container {% templatetag closeblock %} 6 |
7 |
8 | 12 |

{{ project_name }}

13 |
14 | 15 |
16 |

Hello!

17 |

Hope this template helps you out

18 |

Splendid!

19 |
20 | 21 |
22 |
23 |

Template generated with django-aws-template

24 |

25 |
26 | 27 | 30 |
31 | {% templatetag openblock %} endblock container {% templatetag closeblock %} 32 | -------------------------------------------------------------------------------- /server/apps/main/tests.py: -------------------------------------------------------------------------------- 1 | #import unittest 2 | import json 3 | 4 | from django.contrib.auth import get_user_model 5 | from django.core import mail 6 | from django.test import Client, TestCase 7 | 8 | from rest_framework import status 9 | from rest_framework.reverse import reverse 10 | from rest_framework.test import APIClient, APIRequestFactory 11 | 12 | from .models import * 13 | 14 | user_model = get_user_model() 15 | 16 | class MainTestCase(TestCase): 17 | """ 18 | Fixure includes: 19 | """ 20 | #fixtures = ['testdb_main.json'] 21 | 22 | def setUp(self): 23 | self.u1 = user_model.objects.create_superuser(username='User1', email='user1@foo.com', password='pass') 24 | self.u1.is_active = True 25 | self.u1.save() 26 | self.u2 = user_model.objects.create_user(username='User2', email='user2@foo.com', password='pass') 27 | self.u2.is_active = True 28 | self.u2.save() 29 | self.u3 = user_model.objects.create_user(username='User3', email='user3@foo.com', password='pass') 30 | self.u3.is_active = True 31 | self.u3.save() 32 | return 33 | 34 | def tearDown(self): 35 | user_model.objects.all().delete() 36 | 37 | def testPages(self): 38 | response = self.client.get('/') 39 | self.assertEqual(response.status_code, status.HTTP_200_OK) 40 | response = self.client.get('/api/v1/') 41 | self.assertEqual(response.status_code, status.HTTP_200_OK) 42 | response = self.client.get('/robots.txt') 43 | self.assertEqual(response.status_code, status.HTTP_200_OK) 44 | 45 | response = self.client.login(email='user1@foo.com', password='pass') 46 | response = self.client.get('/', {}) 47 | self.assertEqual(response.status_code, status.HTTP_200_OK) 48 | response = self.client.get('/api/v1/') 49 | self.assertEqual(response.status_code, status.HTTP_200_OK) 50 | self.client.logout() 51 | 52 | def testPostContactMessage(self): 53 | 54 | ''' 55 | resp = self.client.post('/api/v1/message', {'name':'M1', 'email':'foo@bar.com', 56 | 'phone':'(650)555-1234', 57 | 'message':'This is a test'}, 58 | format='json') 59 | self.assertEqual(resp.status_code, status.HTTP_201_CREATED) 60 | 61 | resp = self.client.post('/api/v1/message', {'name':'M1', 'email':'foo@bar.com', 62 | 'message':'This is another test from same user'}, 63 | format='json') 64 | self.assertEqual(resp.status_code, status.HTTP_201_CREATED) 65 | 66 | resp = self.client.post('/api/v1/message', {'name':'M2', 'email':'foo@foo.com', 67 | 'message':'This is a test'}, 68 | format='json') 69 | self.assertEqual(resp.status_code, status.HTTP_201_CREATED) 70 | 71 | # Nobody should be able to read 72 | resp = self.client.get('/api/v1/message', data={'format': 'json'}) 73 | self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED) 74 | resp = self.client.get('/api/v1/message/1', data={'format': 'json'}) 75 | self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED) 76 | 77 | # even if logged in but not staff 78 | self.client.login(email='user2@foo.com', password='pass') 79 | 80 | resp = self.client.get('/api/v1/message', data={'format': 'json'}) 81 | self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN) 82 | resp = self.client.get('/api/v1/message/1', data={'format': 'json'}) 83 | self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN) 84 | 85 | self.client.logout() 86 | 87 | # SuperUser or Staff can access it 88 | self.client.login(email='user1@foo.com', password='pass') 89 | 90 | resp = self.client.get('/api/v1/message', data={'format': 'json'}) 91 | self.assertEqual(resp.status_code, status.HTTP_200_OK) 92 | deserialized = json.loads(resp.content) 93 | self.assertEqual(deserialized['count'], 3) 94 | resp = self.client.get('/api/v1/message/1', data={'format': 'json'}) 95 | self.assertEqual(resp.status_code, status.HTTP_200_OK) 96 | 97 | self.client.logout() 98 | ''' 99 | 100 | -------------------------------------------------------------------------------- /server/apps/main/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from django.views.decorators.cache import cache_page 3 | 4 | from .views import * 5 | 6 | urlpatterns = [ 7 | path('', HomeIndexView.as_view(), name='home'), 8 | path('about/', AboutView.as_view(), name='about'), 9 | path('message/send/', ContactCreateView.as_view(), name='send-message'), 10 | # --------------------------------- 11 | path('jsi18n/', i18n_javascript), 12 | path('admin/jsi18n/', i18n_javascript), 13 | path('i18n/', include('django.conf.urls.i18n')), 14 | path('robots.txt', RobotView.as_view()), 15 | path('crossdomain.xml', CrossDomainView.as_view()), 16 | ] 17 | -------------------------------------------------------------------------------- /server/apps/main/views.py: -------------------------------------------------------------------------------- 1 | 2 | import logging 3 | 4 | from django.conf import settings 5 | from django.contrib import admin, messages 6 | from django.contrib.auth.decorators import login_required 7 | from django.http import HttpResponseRedirect 8 | from django.template import RequestContext 9 | from django.urls import reverse 10 | from django.utils.decorators import method_decorator 11 | from django.utils.safestring import mark_safe 12 | from django.utils.translation import ugettext_lazy as _ 13 | from django.views.decorators.csrf import ensure_csrf_cookie 14 | from django.views.generic import CreateView, DetailView, TemplateView, View 15 | 16 | from .forms import * 17 | from .models import * 18 | from .tasks import send_contact_me_notification 19 | 20 | AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User') 21 | 22 | # Get an instance of a logger 23 | logger = logging.getLogger(__name__) 24 | 25 | 26 | class HomeIndexView(TemplateView): 27 | 28 | def get_template_names(self): 29 | user = self.request.user 30 | 31 | if user.is_authenticated: 32 | template_name = 'main/index.html' 33 | else: 34 | template_name = 'main/landing.html' 35 | 36 | return template_name 37 | 38 | def get_context_data(self, **kwargs): 39 | context = super(HomeIndexView, self).get_context_data(**kwargs) 40 | 41 | context['production'] = settings.PRODUCTION 42 | context['recaptcha_public_key'] = settings.RECAPTCHA_PUBLIC_KEY 43 | context['user'] = self.request.user 44 | 45 | return context 46 | 47 | 48 | class RobotView(TemplateView): 49 | template_name = 'robots.txt' 50 | 51 | 52 | def i18n_javascript(request): 53 | return admin.site.i18n_javascript(request) 54 | 55 | 56 | class CrossDomainView(TemplateView): 57 | template_name = 'crossdomain.xml' 58 | 59 | def get_context_data(self, **kwargs): 60 | context = super(CrossDomainView, self).get_context_data(**kwargs) 61 | domains = [] 62 | host = self.request.get_host() 63 | if host: 64 | domains.append(host) 65 | cdn = getattr(settings, 'AWS_S3_CUSTOM_DOMAIN', None) 66 | if cdn: 67 | domains.append(cdn) 68 | 69 | context['extra_domains'] = domains 70 | 71 | return context 72 | 73 | 74 | class ContactCreateView(CreateView): 75 | model = ContactMessage 76 | template_name = 'newForm.html' 77 | 78 | def get_form_class(self): 79 | if self.request.user.is_authenticated: 80 | return ContactPrivateForm 81 | return ContactPublicForm 82 | 83 | def get_context_data(self, **kwargs): 84 | context = super(ContactCreateView, self).get_context_data(**kwargs) 85 | if self.request.user.is_authenticated: 86 | context['referer'] = self.request.META.get('HTTP_REFERER') 87 | else: 88 | context['referer'] = '/' 89 | context['title'] = _('Send us a message') 90 | return context 91 | 92 | def form_valid(self, form): 93 | self.object = form.save(commit=True) 94 | if self.request.user.is_authenticated(): 95 | self.object.name = self.request.user.name 96 | self.object.email = self.request.user.email 97 | self.object.save() 98 | 99 | messages.add_message(self.request, messages.SUCCESS, 'Your message has been sent.') 100 | 101 | return HttpResponseRedirect('/') 102 | 103 | 104 | class AboutView(TemplateView): 105 | template_name = 'main/about.html' -------------------------------------------------------------------------------- /server/apps/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkarchmer/django-aws-template/2bf15740cdad0ff9e6656c3c452a5ee6042cc0a6/server/apps/utils/__init__.py -------------------------------------------------------------------------------- /server/apps/utils/pagination.py: -------------------------------------------------------------------------------- 1 | from rest_framework.pagination import PageNumberPagination 2 | 3 | 4 | class LargeResultsSetPagination(PageNumberPagination): 5 | page_size = 1000 6 | page_size_query_param = 'page_size' 7 | max_page_size = 1000 8 | 9 | class StandardResultsSetPagination(PageNumberPagination): 10 | page_size = 100 11 | page_size_query_param = 'page_size' 12 | max_page_size = 1000 13 | 14 | -------------------------------------------------------------------------------- /server/apps/utils/permissions.py: -------------------------------------------------------------------------------- 1 | __author__ = 'dkarchmer' 2 | 3 | from rest_framework import permissions 4 | 5 | 6 | class IsOwnerOnly(permissions.BasePermission): 7 | """ 8 | Custom permission to only allow owners of an object to access (read/write) 9 | """ 10 | 11 | def has_permission(self, request, view): 12 | 13 | return request.user.is_authenticated() 14 | 15 | def has_object_permission(self, request, view, obj): 16 | 17 | # Write permissions are only allowed to the owner of the snippet 18 | return obj is None or obj.created_by == request.user -------------------------------------------------------------------------------- /server/apps/utils/timezoneMiddleware.py: -------------------------------------------------------------------------------- 1 | __author__ = 'dkarchmer' 2 | 3 | import pytz 4 | 5 | from django.utils import timezone 6 | 7 | 8 | class TimezoneMiddleware: 9 | def __init__(self, get_response): 10 | self.get_response = get_response 11 | # One-time configuration and initialization. 12 | 13 | def __call__(self, request): 14 | # Code to be executed for each request before 15 | # the view (and later middleware) are called. 16 | 17 | response = self.get_response(request) 18 | 19 | # Code to be executed for each request/response after 20 | # the view is called. 21 | 22 | tzname = request.session.get('django_timezone') 23 | 24 | if not tzname: 25 | # Get it from the Account. Should hopefully happens once per session 26 | user = request.user 27 | if user and not user.is_anonymous: 28 | tzname = user.time_zone 29 | if tzname: 30 | request.session['django_timezone'] = tzname 31 | 32 | if tzname: 33 | timezone.activate(pytz.timezone(tzname)) 34 | else: 35 | timezone.deactivate() 36 | 37 | return response 38 | -------------------------------------------------------------------------------- /server/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkarchmer/django-aws-template/2bf15740cdad0ff9e6656c3c452a5ee6042cc0a6/server/config/__init__.py -------------------------------------------------------------------------------- /server/config/runner.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.test.runner import DiscoverRunner 3 | 4 | 5 | class AppsTestSuiteRunner(DiscoverRunner): 6 | 7 | def run_tests(self, test_labels, extra_tests=None, **kwargs): 8 | if not test_labels: 9 | # Only test our apps 10 | test_labels = settings.COMMON_APPS 11 | print ('Testing: ' + str(test_labels)) 12 | 13 | return super(AppsTestSuiteRunner, self).run_tests( 14 | test_labels, extra_tests, **kwargs) 15 | -------------------------------------------------------------------------------- /server/config/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkarchmer/django-aws-template/2bf15740cdad0ff9e6656c3c452a5ee6042cc0a6/server/config/settings/__init__.py -------------------------------------------------------------------------------- /server/config/settings/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for {{ project_name }} project. 3 | For more information on this file, see 4 | https://docs.djangoproject.com/en/dev/topics/settings/ 5 | For the full list of settings and their values, see 6 | https://docs.djangoproject.com/en/dev/ref/settings/ 7 | """ 8 | import os 9 | import sys 10 | 11 | from django.contrib import messages 12 | from django.urls import reverse_lazy 13 | 14 | # Use 12factor inspired environment variables or from a file 15 | import environ 16 | 17 | # Build paths inside the project like this: join(BASE_DIR, "directory") 18 | BASE_PATH = environ.Path(__file__) - 3 19 | BASE_DIR = str(BASE_PATH) 20 | LOG_FILE = BASE_PATH.path('logs') 21 | print('BASE_DIR = ' + BASE_DIR) 22 | 23 | env = environ.Env( 24 | DJANGO_DEBUG=(bool, False), 25 | RECAPTCHA_PUBLIC_KEY=(str, 'Changeme'), 26 | RECAPTCHA_PRIVATE_KEY=(str, 'Changeme'), 27 | PRODUCTION=(bool, False), 28 | DOMAIN_NAME=(str, 'mydomain.com'), 29 | DOMAIN_BASE_URL=(str, 'https://mydomain.com'), 30 | COMPANY_NAME=(str, 'COMPANY_NAME'), 31 | INITIAL_ADMIN_EMAIL=(str, 'admin@mydomain.com'), 32 | DJANGO_ENV_FILE = (str, '.local.env') 33 | ) 34 | 35 | SITE_ID = 1 36 | 37 | # Use Django templates using the new Django 1.8 TEMPLATES settings 38 | TEMPLATES = [ 39 | { 40 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 41 | 'DIRS': [ 42 | os.path.join(BASE_DIR, 'templates'), 43 | # insert more TEMPLATE_DIRS here 44 | ], 45 | 'APP_DIRS': True, 46 | 'OPTIONS': { 47 | 'context_processors': [ 48 | # Insert your TEMPLATE_CONTEXT_PROCESSORS here or use this 49 | # list if you haven't customized them: 50 | 'django.contrib.auth.context_processors.auth', 51 | 'django.template.context_processors.debug', 52 | 'django.template.context_processors.i18n', 53 | 'django.template.context_processors.media', 54 | 'django.template.context_processors.static', 55 | 'django.template.context_processors.tz', 56 | 'django.contrib.messages.context_processors.messages', 57 | 'django.template.context_processors.request', 58 | ], 59 | }, 60 | }, 61 | ] 62 | 63 | # Ideally move env file should be outside the git repo 64 | # i.e. BASE_DIR.parent.parent 65 | env_file = os.path.join(os.path.dirname(__file__), env('DJANGO_ENV_FILE')) 66 | 67 | if os.path.isfile(env_file): 68 | print('Reading Env file: {0}'.format(env_file)) 69 | environ.Env.read_env(env_file) 70 | else: 71 | print('Warning!! No .env file: {0}'.format(env_file)) 72 | 73 | ADMINS = ( 74 | # ('Username', 'your_email@domain.com'), 75 | ('admin', env('INITIAL_ADMIN_EMAIL')), 76 | ) 77 | 78 | # Quick-start development settings - unsuitable for production 79 | # See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/ 80 | 81 | # SECURITY WARNING: keep the secret key used in production secret! 82 | # Raises ImproperlyConfigured exception if SECRET_KEY not in os.environ 83 | SECRET_KEY = env('SECRET_KEY') 84 | 85 | ALLOWED_HOSTS = [] 86 | 87 | PRODUCTION = env('PRODUCTION') 88 | DOMAIN_NAME = env('DOMAIN_NAME') 89 | COMPANY_NAME = env('COMPANY_NAME') 90 | 91 | # Application definition 92 | 93 | DJANGO_APPS = ( 94 | 'django.contrib.auth', 95 | 'django.contrib.admin', 96 | 'django.contrib.contenttypes', 97 | 'django.contrib.sites', 98 | 'django.contrib.sessions', 99 | 'django.contrib.messages', 100 | 'django.contrib.staticfiles', 101 | 'django.contrib.humanize', 102 | ) 103 | 104 | THIRD_PARTY_APPS = ( 105 | 'rest_framework', 106 | 'rest_framework.authtoken', 107 | 'allauth', 108 | 'allauth.account', 109 | 'allauth.socialaccount', 110 | 'corsheaders', 111 | 'captcha', 112 | 'crispy_forms', 113 | ) 114 | 115 | # Apps specific for this project go here. 116 | COMMON_APPS = ( 117 | 'apps.authentication', 118 | 'apps.main', 119 | ) 120 | 121 | INSTALLED_APPS = DJANGO_APPS + COMMON_APPS + THIRD_PARTY_APPS 122 | 123 | MIDDLEWARE = [ 124 | 'django.middleware.security.SecurityMiddleware', 125 | 'django.contrib.sessions.middleware.SessionMiddleware', 126 | 'corsheaders.middleware.CorsMiddleware', 127 | 'django.middleware.common.CommonMiddleware', 128 | 'django.middleware.csrf.CsrfViewMiddleware', 129 | 'corsheaders.middleware.CorsPostCsrfMiddleware', 130 | 'django_feature_policy.FeaturePolicyMiddleware', 131 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 132 | 'django.contrib.messages.middleware.MessageMiddleware', 133 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 134 | 'apps.utils.timezoneMiddleware.TimezoneMiddleware', 135 | ] 136 | 137 | ROOT_URLCONF = 'config.urls' 138 | 139 | WSGI_APPLICATION = 'config.wsgi.application' 140 | 141 | # Database 142 | # https://docs.djangoproject.com/en/dev/ref/settings/#databases 143 | 144 | if PRODUCTION: 145 | DATABASES = { 146 | 'default': { 147 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 148 | 'NAME': os.environ['RDS_DB_NAME'], 149 | 'USER': os.environ['RDS_USERNAME'], 150 | 'PASSWORD': os.environ['RDS_PASSWORD'], 151 | 'HOST': os.environ['RDS_HOSTNAME'], 152 | 'PORT': os.environ['RDS_PORT'], 153 | 'CONN_MAX_AGE': 600, 154 | } 155 | } 156 | else: 157 | DATABASES = { 158 | # Raises ImproperlyConfigured exception if DATABASE_URL not in 159 | # os.environ 160 | 'default': env.db(), 161 | } 162 | 163 | # Internationalization 164 | # https://docs.djangoproject.com/en/dev/topics/i18n/ 165 | 166 | LANGUAGE_CODE = 'en-us' 167 | 168 | TIME_ZONE = 'UTC' 169 | 170 | USE_I18N = True 171 | 172 | USE_L10N = True 173 | 174 | USE_TZ = True 175 | 176 | DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' 177 | 178 | # Static files (CSS, JavaScript, Images) 179 | # https://docs.djangoproject.com/en/dev/howto/static-files/ 180 | 181 | ROOT_DIR = environ.Path(__file__) - 4 182 | STATIC_ROOT = str(ROOT_DIR.path('staticfiles')) 183 | STATICFILES_DIRS = [] 184 | 185 | STATICFILES_FINDERS = ( 186 | 'django.contrib.staticfiles.finders.FileSystemFinder', 187 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 188 | ) 189 | 190 | 191 | # Crispy Form Theme - Bootstrap 3 192 | CRISPY_TEMPLATE_PACK = 'bootstrap3' 193 | 194 | # For Bootstrap 3, change error alert to 'danger' 195 | MESSAGE_TAGS = { 196 | messages.ERROR: 'danger' 197 | } 198 | 199 | # Authentication Settings 200 | AUTH_USER_MODEL = 'authentication.Account' 201 | #LOGIN_REDIRECT_URL = reverse_lazy("profiles:show_self") 202 | #LOGIN_URL = reverse_lazy("accounts:login") 203 | 204 | # Recaptcha https://www.google.com/recaptcha/admin 205 | # https://github.com/praekelt/django-recaptcha 206 | RECAPTCHA_PUBLIC_KEY = env('RECAPTCHA_PUBLIC_KEY') 207 | RECAPTCHA_PRIVATE_KEY = env('RECAPTCHA_PRIVATE_KEY') 208 | NOCAPTCHA = True 209 | RECAPTCHA_USE_SSL = True 210 | 211 | # https://github.com/ottoyiu/django-cors-headers/ 212 | CORS_ALLOW_CREDENTIALS = True 213 | CORS_ORIGIN_ALLOW_ALL = True 214 | CORS_URLS_REGEX = r'^/api.*$' 215 | CORS_ORIGIN_WHITELIST = ( 216 | 'https://mydomain.com', 217 | 'https://xxxxxxxxxx.cloudfront.net', 218 | ) 219 | 220 | CSRF_COOKIE_HTTPONLY = False # Most be False for javascript APIs to be able to post/put/delete 221 | SESSION_COOKIE_HTTPONLY = True 222 | 223 | # http://www.django-rest-framework.org/ 224 | REST_FRAMEWORK = { 225 | # Use hyperlinked styles by default. 226 | # Only used if the `serializer_class` attribute is not set on a view. 227 | 'DEFAULT_MODEL_SERIALIZER_CLASS': 228 | 'rest_framework.serializers.HyperlinkedModelSerializer', 229 | 230 | # Use Django's standard `django.contrib.auth` permissions, 231 | # or allow read-only access for unauthenticated users. 232 | 'DEFAULT_PERMISSION_CLASSES': ( 233 | 'rest_framework.permissions.AllowAny', 234 | ), 235 | 236 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 237 | 'rest_framework.authentication.TokenAuthentication', 238 | 'rest_framework.authentication.SessionAuthentication', 239 | ), 240 | 241 | 'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',), 242 | 'DEFAULT_PAGINATION_CLASS': 'apps.utils.pagination.StandardResultsSetPagination' 243 | } 244 | 245 | # auth and allauth settings 246 | AUTHENTICATION_BACKENDS = ( 247 | # Needed to login by username in Django admin, regardless of `allauth` 248 | "django.contrib.auth.backends.ModelBackend", 249 | # `allauth` specific authentication methods, such as login by e-mail 250 | "allauth.account.auth_backends.AuthenticationBackend" 251 | ) 252 | 253 | LOGIN_REDIRECT_URL = '/' 254 | LOGIN_URL = '/account/login/' 255 | SOCIALACCOUNT_QUERY_EMAIL = True 256 | SOCIALACCOUNT_AUTO_SIGNUP = True 257 | SOCIALACCOUNT_EMAIL_VERIFICATION = "none" 258 | #SOCIALACCOUNT_EMAIL_REQUIRED = False 259 | ''' 260 | SOCIALACCOUNT_PROVIDERS = { 261 | 'facebook': { 262 | 'SCOPE': ['email', 'public_profile'], 263 | #'AUTH_PARAMS': {'auth_type': 'reauthenticate'}, 264 | 'METHOD': 'oauth2', 265 | 'VERSION': 'v2.4', 266 | 'FIELDS': [ 267 | 'id', 268 | 'email', 269 | 'name', 270 | 'first_name', 271 | 'last_name', 272 | 'verified', 273 | 'locale', 274 | 'timezone', 275 | 'link', 276 | ], 277 | }, 278 | 'google': { 279 | 'SCOPE': ['profile', 'email'], 280 | 'AUTH_PARAMS': { 'access_type': 'online' } 281 | } 282 | } 283 | ''' 284 | ACCOUNT_USER_MODEL_USERNAME_FIELD = 'username' 285 | ACCOUNT_USER_MODEL_EMAIL_FIELD= 'email' 286 | ACCOUNT_UNIQUE_EMAIL = True 287 | ACCOUNT_USERNAME_REQUIRED = True 288 | ACCOUNT_AUTHENTICATION_METHOD = 'email' 289 | ACCOUNT_EMAIL_REQUIRED = True 290 | ACCOUNT_EMAIL_VERIFICATION = 'mandatory' 291 | ACCOUNT_FORMS = { 292 | 'login': 'apps.authentication.forms.AllauthLoginForm', 293 | 'signup': 'apps.authentication.forms.AllauthSignupForm' 294 | } 295 | 296 | if PRODUCTION: 297 | # For production, hard code to file created by .ebextensions 298 | LOG_FILEPATH = '/opt/python/log/my.log' 299 | else: 300 | LOG_FILEPATH = os.path.join(str(LOG_FILE), 'server.log') 301 | 302 | 303 | LOGGING = { 304 | 'version': 1, 305 | 'disable_existing_loggers': False, 306 | 'formatters': { 307 | 'verbose': { 308 | 'format': "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s", 309 | 'datefmt': "%d/%b/%Y %H:%M:%S" 310 | }, 311 | 'simple': { 312 | 'format': '%(levelname)s %(message)s' 313 | }, 314 | }, 315 | 'filters': { 316 | 'require_debug_true': { 317 | '()': 'django.utils.log.RequireDebugTrue', 318 | }, 319 | }, 320 | 'handlers': { 321 | 'default': { 322 | 'level': 'DEBUG', 323 | 'class': 'logging.handlers.RotatingFileHandler', 324 | 'filename': LOG_FILEPATH, 325 | 'maxBytes': 1024 * 1024 * 5, # 5MB 326 | 'backupCount': 5, 327 | 'formatter': 'simple' 328 | }, 329 | 'console': { 330 | 'level': 'INFO', 331 | 'class': 'logging.StreamHandler', 332 | 'formatter': 'simple' 333 | }, 334 | 'mail_admins': { 335 | 'level': 'ERROR', 336 | 'class': 'django.utils.log.AdminEmailHandler', 337 | } 338 | }, 339 | 'loggers': { 340 | '': { 341 | 'handlers': ['default', 'console'], 342 | 'level': 'DEBUG', 343 | 'propagate': True 344 | }, 345 | 'django': { 346 | 'handlers': ['console'], 347 | 'propagate': True, 348 | }, 349 | 'django.request': { 350 | 'handlers': ['mail_admins'], 351 | 'level': 'ERROR', 352 | 'propagate': False, 353 | }, 354 | } 355 | } 356 | 357 | if not PRODUCTION: 358 | print('PRODUCTION=False') 359 | AWS_ACCESS_KEY_ID = env.str('AWS_ACCESS_KEY_ID') 360 | AWS_SECRET_ACCESS_KEY = env.str('AWS_SECRET_ACCESS_KEY') 361 | 362 | # BOTO may need an actual Env Variable, so set it 363 | os.environ['AWS_ACCESS_KEY_ID'] = AWS_ACCESS_KEY_ID 364 | os.environ['AWS_SECRET_ACCESS_KEY'] = AWS_SECRET_ACCESS_KEY 365 | 366 | TEST_RUNNER = 'config.runner.AppsTestSuiteRunner' -------------------------------------------------------------------------------- /server/config/settings/development.py: -------------------------------------------------------------------------------- 1 | import logging.config 2 | import sys 3 | 4 | from .base import * # NOQA 5 | 6 | # SECURITY WARNING: don't run with debug turned on in production! 7 | DEBUG = True 8 | TEMPLATES[0]['OPTIONS'].update({'debug': True}) 9 | 10 | STATIC_ROOT = str(ROOT_DIR.path('staticfiles')) 11 | STATIC_URL = '/staticfiles/' 12 | STATICFILES_DIRS = ( 13 | ('dist', os.path.join(STATIC_ROOT, 'dist')), 14 | ) 15 | 16 | # Turn off debug while imported by Celery with a workaround 17 | # See http://stackoverflow.com/a/4806384 18 | if "celery" in sys.argv[0]: 19 | DEBUG = False 20 | 21 | # Show emails to console in DEBUG mode 22 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 23 | 24 | # Show thumbnail generation errors 25 | THUMBNAIL_DEBUG = True 26 | 27 | 28 | # Debug Toolbar (http://django-debug-toolbar.readthedocs.org/) 29 | INSTALLED_APPS += ('debug_toolbar',) 30 | DEBUG_TOOLBAR_PATCH_SETTINGS = False 31 | MIDDLEWARE += ('debug_toolbar.middleware.DebugToolbarMiddleware',) 32 | INTERNAL_IPS = ('127.0.0.1', '192.168.99.100',) 33 | DEBUG_TOOLBAR_PANELS = [ 34 | 'debug_toolbar.panels.versions.VersionsPanel', 35 | 'debug_toolbar.panels.timer.TimerPanel', 36 | 'debug_toolbar.panels.settings.SettingsPanel', 37 | 'debug_toolbar.panels.headers.HeadersPanel', 38 | 'debug_toolbar.panels.request.RequestPanel', 39 | 'debug_toolbar.panels.sql.SQLPanel', 40 | # 'debug_toolbar.panels.staticfiles.StaticFilesPanel', 41 | 'debug_toolbar.panels.templates.TemplatesPanel', 42 | 'debug_toolbar.panels.signals.SignalsPanel', 43 | 'debug_toolbar.panels.logging.LoggingPanel', 44 | 'debug_toolbar.panels.redirects.RedirectsPanel', 45 | ] 46 | 47 | -------------------------------------------------------------------------------- /server/config/settings/docker.py: -------------------------------------------------------------------------------- 1 | import logging.config 2 | import sys 3 | 4 | from .base import * # NOQA 5 | 6 | # SECURITY WARNING: don't run with debug turned on in production! 7 | DEBUG = True 8 | TEMPLATES[0]['OPTIONS'].update({'debug': True}) 9 | 10 | STATIC_URL = '/static/' 11 | STATIC_ROOT = '/www/static/' 12 | STATICFILES_DIRS = ( 13 | ('dist', os.path.join(STATIC_ROOT, 'dist')), 14 | ) 15 | 16 | # Turn off debug while imported by Celery with a workaround 17 | # See http://stackoverflow.com/a/4806384 18 | if "celery" in sys.argv[0]: 19 | DEBUG = False 20 | 21 | # Show emails to console in DEBUG mode 22 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 23 | 24 | # Show thumbnail generation errors 25 | THUMBNAIL_DEBUG = True 26 | 27 | # Debug Toolbar (http://django-debug-toolbar.readthedocs.org/) 28 | INSTALLED_APPS += ('debug_toolbar',) 29 | DEBUG_TOOLBAR_PATCH_SETTINGS = False 30 | MIDDLEWARE += ('debug_toolbar.middleware.DebugToolbarMiddleware',) 31 | INTERNAL_IPS = ('127.0.0.1', '192.168.99.100',) 32 | DEBUG_TOOLBAR_PANELS = [ 33 | 'debug_toolbar.panels.versions.VersionsPanel', 34 | 'debug_toolbar.panels.timer.TimerPanel', 35 | 'debug_toolbar.panels.settings.SettingsPanel', 36 | 'debug_toolbar.panels.headers.HeadersPanel', 37 | 'debug_toolbar.panels.request.RequestPanel', 38 | 'debug_toolbar.panels.sql.SQLPanel', 39 | # 'debug_toolbar.panels.staticfiles.StaticFilesPanel', 40 | 'debug_toolbar.panels.templates.TemplatesPanel', 41 | 'debug_toolbar.panels.signals.SignalsPanel', 42 | 'debug_toolbar.panels.logging.LoggingPanel', 43 | 'debug_toolbar.panels.redirects.RedirectsPanel', 44 | ] 45 | 46 | -------------------------------------------------------------------------------- /server/config/settings/production.py: -------------------------------------------------------------------------------- 1 | # In production set the environment variable like this: 2 | # DJANGO_SETTINGS_MODULE=config.settings.production 3 | import logging.config 4 | import socket 5 | 6 | from .base import * # NOQA 7 | 8 | # For security and performance reasons, DEBUG is turned off 9 | DEBUG = False 10 | TEMPLATE_DEBUG = False 11 | 12 | enable_security = True 13 | if enable_security: 14 | SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 15 | SESSION_COOKIE_SECURE = True 16 | CSRF_COOKIE_SECURE = True 17 | 18 | ''' 19 | The following comes from: 20 | http://security.stackexchange.com/questions/8964/trying-to-make-a-django-based-site-use-https-only-not-sure-if-its-secure/8970#comment80472_8970 21 | ''' 22 | os.environ['HTTPS'] = "on" 23 | # Must mention ALLOWED_HOSTS in production! 24 | local_ip = str(socket.gethostbyname(socket.gethostname())) 25 | print ('hostname: ' + socket.gethostname()) 26 | print ('hostbyname: ' + local_ip) 27 | 28 | # ALLOWED_HOSTS=[local_ip, '.mydomain.com', 'myapp.elasticbeanstalk.com' ] 29 | else: 30 | ALLOWED_HOSTS = ['*', ] 31 | print('**********************************') 32 | print('**********************************') 33 | print('WARNING: Disable security features') 34 | print('**********************************') 35 | print('**********************************') 36 | 37 | 38 | # Cache the templates in memory for speed-up 39 | loaders = [ 40 | ('django.template.loaders.cached.Loader', [ 41 | 'django.template.loaders.filesystem.Loader', 42 | 'django.template.loaders.app_directories.Loader', 43 | ]), 44 | ] 45 | 46 | TEMPLATES[0]['OPTIONS'].update({"loaders": loaders}) 47 | TEMPLATES[0].update({"APP_DIRS": False}) 48 | 49 | AWS_STORAGE_BUCKET_NAME = env.str('AWS_STORAGE_BUCKET_NAME') 50 | AWS_MEDIA_BUCKET_NAME = env.str('AWS_MEDIA_BUCKET_NAME') 51 | 52 | # Define STATIC_ROOT for the collectstatic command 53 | #STATICFILES_STORAGE = 'storages.backends.s3boto.S3BotoStorage' 54 | # Setup CloudFront 55 | AWS_S3_URL_PROTOCOL = 'https' 56 | # Enable one AWS_S3_CUSTOM_DOMAIN to use cloudfront 57 | # AWS_S3_CUSTOM_DOMAIN = 'xxxxxxx.cloudfront.net' 58 | AWS_S3_CUSTOM_DOMAIN = 'cdn.mydomain.com' 59 | # STATIC_URL = S3_STATIC_URL + STATIC_DIRECTORY - Use to host statics on S3 only 60 | STATIC_URL = AWS_S3_URL_PROTOCOL + "://" + AWS_S3_CUSTOM_DOMAIN + '/static/' 61 | 62 | -------------------------------------------------------------------------------- /server/config/settings/sample-docker.env: -------------------------------------------------------------------------------- 1 | # Django project configuration, if environ vars are missing 2 | # 3 | # This is a sample file. Rename to local.env for a quick development 4 | # settings. Git will not track local.env as it contains confidential 5 | # information. So store a backup of this file outside this repo. 6 | # 7 | # Note: No spaces around '=' sign and no quotes for righthand values. 8 | 9 | PRODUCTION=False 10 | DEBUG=True 11 | 12 | # Set postgres based on Docker Address 13 | DATABASE_URL=postgres://postgres@127.0.0.1:5432/database 14 | 15 | # Command to create a new secret key: 16 | # $ python -c 'import random; import string; print("".join([random.SystemRandom().choice(string.digits + string.ascii_letters + string.punctuation) for i in range(100)]))' 17 | SECRET_KEY={{ secret_key }} 18 | 19 | DOMAIN_NAME=127.0.0.1:8000 20 | DOMAIN_BASE_URL=http://127.0.0.1:8000 21 | COMPANY_NAME={{ project_name }} - Test 22 | INITIAL_ADMIN_EMAIL=admin@mydomain.com 23 | 24 | # This should NOT be your production keys, but a set that can only access your Development AWS setup 25 | AWS_ACCESS_KEY_ID=need-value 26 | AWS_SECRET_ACCESS_KEY=need-value 27 | -------------------------------------------------------------------------------- /server/config/settings/sample-local.env: -------------------------------------------------------------------------------- 1 | # Django project configuration, if environ vars are missing 2 | # 3 | # This is a sample file. Rename to local.env for a quick development 4 | # settings. Git will not track local.env as it contains confidential 5 | # information. So store a backup of this file outside this repo. 6 | # 7 | # Note: No spaces around '=' sign and no quotes for righthand values. 8 | 9 | PRODUCTION=False 10 | DEBUG=True 11 | 12 | # Use SQLite3 for local development 13 | DATABASE_URL=sqlite:///db.sqlite3 14 | 15 | # Command to create a new secret key: 16 | # $ python -c 'import random; import string; print("".join([random.SystemRandom().choice(string.digits + string.ascii_letters + string.punctuation) for i in range(100)]))' 17 | SECRET_KEY={{ secret_key }} 18 | 19 | DOMAIN_NAME=127.0.0.1:8000 20 | DOMAIN_BASE_URL=http://127.0.0.1:8000 21 | COMPANY_NAME={{ project_name }} - Test 22 | INITIAL_ADMIN_EMAIL=admin@mydomain.com 23 | 24 | # This should NOT be your production keys, but a set that can only access your Development AWS setup 25 | AWS_ACCESS_KEY_ID=need-value 26 | AWS_SECRET_ACCESS_KEY=need-value 27 | -------------------------------------------------------------------------------- /server/config/settings/sample-production.env: -------------------------------------------------------------------------------- 1 | # Django project configuration, if environ vars are missing 2 | # 3 | # This is a sample file. Rename to local.env for a quick development 4 | # settings. Git will not track local.env as it contains confidential 5 | # information. So store a backup of this file outside this repo. 6 | # 7 | # Note: No spaces around '=' sign and no quotes for righthand values. 8 | 9 | DEBUG=False 10 | 11 | # Should be the RDS database address 12 | DATABASE_URL=postgres://username:password@127.0.0.1:5432/database 13 | 14 | # Command to create a new secret key: 15 | # $ python -c 'import random; import string; print("".join([random.SystemRandom().choice(string.digits + string.ascii_letters + string.punctuation) for i in range(100)]))' 16 | SECRET_KEY={{ secret_key }} 17 | 18 | DOMAIN_NAME=mydomain.com 19 | DOMAIN_BASE_URL=https://mydomain.com 20 | COMPANY_NAME={{ project_name }} 21 | INITIAL_ADMIN_EMAIL=admin@mydomain.com 22 | 23 | # The AWS_STORAGE_BUCKET_NAME is the one you need for your static files 24 | AWS_STORAGE_BUCKET_NAME=mystaticbucket 25 | AWS_MEDIA_BUCKET_NAME=mymediabucket -------------------------------------------------------------------------------- /server/config/settings/sample-test.env: -------------------------------------------------------------------------------- 1 | # Django project configuration, if environ vars are missing 2 | # 3 | # This is a sample file. Rename to local.env for a quick development 4 | # settings. Git will not track local.env as it contains confidential 5 | # information. So store a backup of this file outside this repo. 6 | # 7 | # Note: No spaces around '=' sign and no quotes for righthand values. 8 | 9 | # All env should be defined on docker-compose.utest.env 10 | -------------------------------------------------------------------------------- /server/config/settings/test.py: -------------------------------------------------------------------------------- 1 | import logging.config 2 | import sys 3 | 4 | from .base import * # NOQA 5 | 6 | # SECURITY WARNING: don't run with debug turned on in production! 7 | DEBUG = True 8 | TEMPLATES[0]['OPTIONS'].update({'debug': True}) 9 | 10 | TIME_ZONE = 'US/Pacific' 11 | 12 | STATIC_ROOT = str(ROOT_DIR.path('staticfiles')) 13 | STATIC_URL = '/staticfiles/' 14 | STATICFILES_DIRS = ( 15 | ('dist', os.path.join(STATIC_ROOT, 'dist')), 16 | ) 17 | 18 | LOGGING['loggers']['']['level'] = 'WARNING' 19 | 20 | DATABASES = { 21 | 'default': { 22 | 'ENGINE': 'django.db.backends.sqlite3', 23 | 'NAME': 'default.sqlite', 24 | } 25 | } 26 | print(str(DATABASES)) 27 | 28 | # Turn off debug while imported by Celery with a workaround 29 | # See http://stackoverflow.com/a/4806384 30 | if "celery" in sys.argv[0]: 31 | DEBUG = False 32 | 33 | # Debug Toolbar (http://django-debug-toolbar.readthedocs.org/) 34 | # By default (for development), show emails to console in DEBUG mode 35 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 36 | #EMAIL_BACKEND = 'django_ses.SESBackend' 37 | print('EMAIL_BACKEND = {0}'.format(EMAIL_BACKEND)) 38 | 39 | CORS_ORIGIN_WHITELIST += ('http://localhost:8000',) 40 | 41 | # Kinesis Firehose: 42 | # ----------------- 43 | USE_FIREHOSE = False 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /server/config/urls.py: -------------------------------------------------------------------------------- 1 | """server URL Configuration 2 | """ 3 | from django.conf import settings 4 | from django.contrib import admin 5 | from django.urls import include, path 6 | 7 | from rest_framework import routers 8 | 9 | from apps.authentication.api_views import AccountViewSet 10 | from apps.main.api_views import APIMessageViewSet 11 | 12 | # Rest APIs 13 | # ========= 14 | v1_api_router = routers.DefaultRouter(trailing_slash=False) 15 | v1_api_router.register(r'account', AccountViewSet) 16 | v1_api_router.register(r'message', APIMessageViewSet) 17 | 18 | admin.autodiscover() 19 | 20 | urlpatterns = [ 21 | 22 | path('', include('apps.main.urls')), 23 | path('account/', include('apps.authentication.urls')), 24 | 25 | path('admin/', admin.site.urls), 26 | 27 | # Rest API 28 | path('api/v1/', include(v1_api_router.urls)), 29 | path('api/v1/auth/', include('apps.authentication.api_urls')), 30 | ] 31 | 32 | if settings.DEBUG and 'debug_toolbar' in settings.INSTALLED_APPS: 33 | import debug_toolbar 34 | urlpatterns.append( 35 | path('__debug__/', include(debug_toolbar.urls)), 36 | ) 37 | -------------------------------------------------------------------------------- /server/config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for server 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.9/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", "config.settings.development") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /server/local-env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # type: source local-env.sh 3 | export DJANGO_SETTINGS_MODULE=config.settings.development 4 | export DJANGO_SERVER_MODE=Test 5 | export DJANGO_SECRET_KEY=dummy 6 | export AWS_PROFILE=myprofile 7 | export DJANGO_ENV_FILE=.local.env 8 | export PRODUCTION=0 -------------------------------------------------------------------------------- /server/locale/README.md: -------------------------------------------------------------------------------- 1 | * Directory needed for Django's translation infrastructure 2 | 3 | `python manage.py ` -------------------------------------------------------------------------------- /server/logs/.keep: -------------------------------------------------------------------------------- 1 | 2 | ** Keep this directory -------------------------------------------------------------------------------- /server/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", "config.settings.development") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /server/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --nomigrations --reuse-db 3 | DJANGO_SETTINGS_MODULE = config.settings.test 4 | python_files = tests.py -------------------------------------------------------------------------------- /server/requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements/production.txt -------------------------------------------------------------------------------- /server/requirements/base.in: -------------------------------------------------------------------------------- 1 | Django 2 | django-allauth 3 | django-cors-headers 4 | django-crispy-forms 5 | django-environ 6 | django-filter 7 | django-health-check 8 | django-redis 9 | djangorestframework 10 | djangorestframework-jwt 11 | drf-yasg 12 | hiredis 13 | psycopg2-binary 14 | pytz==2018.7 15 | raven 16 | boto3 17 | -------------------------------------------------------------------------------- /server/requirements/base.txt: -------------------------------------------------------------------------------- 1 | django==3.2.10 2 | django-environ==0.4.5 3 | django-crispy-forms==1.13.0 4 | djangorestframework==3.13.1 5 | django-filter==21.1 6 | django-cors-headers==3.4.0 7 | django-allauth==0.47.0 8 | django-recaptcha>=1.2.0 9 | django-feature-policy==3.4.0 10 | pytz==2018.7 11 | redis==2.10.6 12 | hiredis==1.1.0 13 | django-redis==4.4.4 14 | boto3>=1.7.19 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /server/requirements/development.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | django-debug-toolbar>=1.6 3 | pytest-django>=3.1.2 4 | pytest-cov>=2.4.0 5 | -------------------------------------------------------------------------------- /server/requirements/docker.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | psycopg2-binary==2.8.5 3 | django-debug-toolbar>=1.6 4 | pytest-django>=3.1.2 5 | pytest-cov>=2.4.0 6 | -------------------------------------------------------------------------------- /server/requirements/production.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | 3 | # Extra stuff required for production like Prod Database interfacing packages 4 | psycopg2-binary==2.8.5 5 | -------------------------------------------------------------------------------- /server/runserver.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd /var/app 4 | export PYTHONPATH=/var/app;$PYTHONPATH 5 | 6 | /usr/local/bin/gunicorn --log-level info --log-file=- --workers 4 --name project_gunicorn -b 0.0.0.0:8000 --reload config.wsgi:application -------------------------------------------------------------------------------- /server/runtest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd /var/app 4 | export PYTHONPATH=/var/app;$PYTHONPATH 5 | 6 | python manage.py test 7 | -------------------------------------------------------------------------------- /server/server-init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Parse arguments in form of --ignore hostname 3 | waitfor=([db]=1) 4 | 5 | # Then we will not wait for hostnames that we ignore 6 | if ((${waitfor[db]} == 1)) 7 | then 8 | while ! nc -zw1 $RDS_HOSTNAME $RDS_PORT; do 9 | echo "Database not found on network." 10 | sleep 1 11 | done 12 | fi 13 | 14 | python manage.py migrate --noinput 15 | python manage.py init-basic-data 16 | 17 | # Run a security check 18 | python manage.py check --deploy 19 | 20 | echo "========= DONE ============" 21 | -------------------------------------------------------------------------------- /server/templates/account/login.html: -------------------------------------------------------------------------------- 1 | {% templatetag openblock %} extends "account/base.html" {% templatetag closeblock %} 2 | 3 | {% templatetag openblock %} load crispy_forms_tags {% templatetag closeblock %} 4 | {% templatetag openblock %} load i18n {% templatetag closeblock %} 5 | {% templatetag openblock %} load account {% templatetag closeblock %} 6 | 7 | {% templatetag openblock %} block head_title {% templatetag closeblock %} 8 | {% templatetag openblock %} trans "Sign In" {% templatetag closeblock %} 9 | {% templatetag openblock %} endblock {% templatetag closeblock %} 10 | 11 | {% templatetag openblock %} block content {% templatetag closeblock %} 12 | 13 |

{% templatetag openblock %} trans "Sign In" {% templatetag closeblock %}

14 | 15 |

16 | {% templatetag openblock %} blocktrans {% templatetag closeblock %} 17 | If you have not created an account yet, then please 18 | sign up first. 19 | {% templatetag openblock %} endblocktrans {% templatetag closeblock %} 20 |

21 | 22 |
23 |
24 | 25 | {% templatetag openblock %} crispy form {% templatetag closeblock %} 26 | 27 |
28 |
29 | 30 | {% templatetag openblock %} endblock {% templatetag closeblock %} -------------------------------------------------------------------------------- /server/templates/account/logout.html: -------------------------------------------------------------------------------- 1 | {% templatetag openblock %} extends "account/base.html" {% templatetag closeblock %} 2 | 3 | {% templatetag openblock %} load i18n {% templatetag closeblock %} 4 | 5 | {% templatetag openblock %} block head_title {% templatetag closeblock %} 6 | {% templatetag openblock %} trans "Sign Out" {% templatetag closeblock %} 7 | {% templatetag openblock %} endblock {% templatetag closeblock %} 8 | 9 | {% templatetag openblock %} block content {% templatetag closeblock %} 10 |

{% templatetag openblock %} trans "Sign Out" {% templatetag closeblock %}

11 | 12 |

{% templatetag openblock %} trans 'Are you sure you want to sign out?' {% templatetag closeblock %}

13 | 14 |
15 | {% templatetag openblock %} csrf_token {% templatetag closeblock %} 16 | {% templatetag openblock %} if redirect_field_value {% templatetag closeblock %} 17 | 18 | {% templatetag openblock %} endif {% templatetag closeblock %} 19 | 22 |
23 | 24 | 25 | {% templatetag openblock %} endblock {% templatetag closeblock %} -------------------------------------------------------------------------------- /server/templates/account/signup.html: -------------------------------------------------------------------------------- 1 | {% templatetag openblock %} extends "account/base.html" {% templatetag closeblock %} 2 | 3 | {% templatetag openblock %} load crispy_forms_tags {% templatetag closeblock %} 4 | {% templatetag openblock %} load i18n {% templatetag closeblock %} 5 | 6 | {% templatetag openblock %} block head_title {% templatetag closeblock %} 7 | {% templatetag openblock %} trans "Sign Up" {% templatetag closeblock %} 8 | {% templatetag openblock %} endblock {% templatetag closeblock %} 9 | 10 | {% templatetag openblock %} block content {% templatetag closeblock %} 11 | 12 |

{% templatetag openblock %} trans "Sign Up" {% templatetag closeblock %}

13 | 14 |
15 |
16 | 17 | {% templatetag openblock %} crispy form {% templatetag closeblock %} 18 | 19 |
20 |
21 | 22 | {% templatetag openblock %} endblock {% templatetag closeblock %} -------------------------------------------------------------------------------- /server/templates/base.html: -------------------------------------------------------------------------------- 1 | {% templatetag openblock %} extends "dist/webapp/index.html" {% templatetag closeblock %} 2 | {% templatetag openblock %} load i18n {% templatetag closeblock %} 3 | {% templatetag openblock %} load static {% templatetag closeblock %} 4 | 5 | 6 | {% templatetag openblock %} block body {% templatetag closeblock %} 7 |
8 |
9 | {% templatetag openblock %} block navbar {% templatetag closeblock %} 10 | 19 |

{{ project_name }}

20 | {% templatetag openblock %} endblock {% templatetag closeblock %} 21 |
22 | 23 | {% templatetag openblock %} block messages {% templatetag closeblock %} 24 | {% templatetag openblock %} if messages {% templatetag closeblock %} 25 | {% templatetag openblock %} for message in messages {% templatetag closeblock %} 26 |
27 | × 28 | {% templatetag openvariable %} message|safe {% templatetag closevariable %} 29 |
30 | {% templatetag openblock %} endfor {% templatetag closeblock %} 31 | {% templatetag openblock %} endif {% templatetag closeblock %} 32 | {% templatetag openblock %} endblock {% templatetag closeblock %} 33 | 34 | {% templatetag openblock %} block content {% templatetag closeblock %} 35 |
36 |

'Allo, 'Allo!

37 |

Always a pleasure scaffolding your apps.

38 |

Splendid!

39 |
40 | 41 |
42 |
43 |

Template generated with django-aws-template

44 |

45 |
46 | {% templatetag openblock %} endblock {% templatetag closeblock %} 47 | 48 | 51 |
52 | {% templatetag openblock %} endblock {% templatetag closeblock %} 53 | 54 | 55 | -------------------------------------------------------------------------------- /server/templates/form.html: -------------------------------------------------------------------------------- 1 | {% templatetag openblock %} extends "base.html" {% templatetag closeblock %} 2 | {% templatetag openblock %} load static {% templatetag closeblock %} 3 | {% templatetag openblock %} load i18n {% templatetag closeblock %} 4 | {% templatetag openblock %} load tz {% templatetag closeblock %} 5 | 6 | {# Load CSS and JavaScript #} 7 | 8 | {% templatetag openblock %} block js {% templatetag closeblock %} 9 | 10 | {% templatetag openblock %} endblock {% templatetag closeblock %} 11 | 12 | {% templatetag openblock %} block media {% templatetag closeblock %} 13 | 14 | {{ form.media }} 15 | 16 | {% templatetag openblock %} endblock {% templatetag closeblock %} 17 | 18 | {% templatetag openblock %} block content {% templatetag closeblock %} 19 | 20 | {% templatetag openblock %} if title {% templatetag closeblock %} 21 |

{{ title }}

22 | {% templatetag openblock %} endif {% templatetag closeblock %} 23 | 24 |
25 |
26 | 27 |
28 | {% templatetag openblock %} csrf_token {% templatetag closeblock %} 29 | 30 | {{ form }} 31 | 32 | CANCEL 33 | 36 |
37 | 38 |
39 |
40 | 41 | 42 | {% templatetag openblock %} endblock {% templatetag closeblock %} -------------------------------------------------------------------------------- /server/templates/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | 3 | Disallow: /static/ 4 | Disallow: /admin/ 5 | Disallow: /api/ 6 | 7 | -------------------------------------------------------------------------------- /server/wsgi.conf: -------------------------------------------------------------------------------- 1 | 2 | LoadModule wsgi_module modules/mod_wsgi.so 3 | WSGIPythonHome /opt/python/run/baselinenv 4 | WSGISocketPrefix run/wsgi 5 | WSGIRestrictEmbedded On 6 | 7 | 8 | 9 | Alias /static/ /opt/python/current/app/static/ 10 | 11 | Order allow,deny 12 | Allow from all 13 | 14 | 15 | 16 | WSGIScriptAlias / /opt/python/current/app/config/wsgi.py 17 | 18 | 19 | 20 | Require all granted 21 | 22 | 23 | WSGIDaemonProcess wsgi processes=1 threads=15 display-name=%{GROUP} \ 24 | python-path=/opt/python/current/app:/opt/python/run/venv/lib64/python3.4/site-packages:/opt/python/run/venv/lib/python3.4/site-packages user=wsgi group=wsgi \ 25 | home=/opt/python/current/app 26 | WSGIProcessGroup wsgi 27 | 28 | RewriteEngine On 29 | RewriteCond %{HTTP:X-Forwarded-Proto} !https 30 | RewriteRule !/about https://%{SERVER_NAME}%{REQUEST_URI} [L,R=301] 31 | 32 | 33 | 34 | LogFormat "%h (%{X-Forwarded-For}i) %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined 35 | 36 | WSGIPassAuthorization On 37 | 38 | -------------------------------------------------------------------------------- /staticfiles/README.md: -------------------------------------------------------------------------------- 1 | * This is the location where the staticbase gulp base project will output the dist directory 2 | * For local development, all static files will be served from this directory 3 | * `python manage.py collecstatics` will also output all static files for django packages -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from invoke import run, task 4 | 5 | PROJECT_NAME = 'aws_template' 6 | 7 | # EDIT with your own settings 8 | AWS_PROFILE = 'myprofile' 9 | AWS_REGION = 'us-east-1' 10 | 11 | # EDIT with your own settings 12 | DEFAULT_SERVER_APP_NAME = f'{PROJECT_NAME.lower()}' 13 | DEFAULT_SERVER_ENV_NAME = f'{PROJECT_NAME.lower()}-prod' 14 | 15 | PROFILE_OPT = '--profile {profile}'.format(profile=AWS_PROFILE) 16 | REGION_OPT = '--region {region}'.format(region=AWS_REGION) 17 | 18 | SERVER_AMI = '64bit Amazon Linux 2 v3.1.1 running Python 3.7' 19 | # SERVER_AMI = '64bit Amazon Linux 2018.03 v2.10.6 running Python 3.6' 20 | 21 | SERVER_INSTANCE_TYPE = 't2.micro' 22 | # Use Elastic Beanstalk managed RDS database early during development 23 | # But it is recommended to later switch to your own RDS outside EB, especially for production. 24 | # This makes it easy to destroy the EB environment without destroying the database 25 | # Note that you will need to set the env variables on .ebextensions/01_main.config 26 | DB_CMD = '-db -db.i db.t2.micro -db.engine postgres -db.version 9.5 -db.user ebroot -db.pass pass.DB' 27 | 28 | CDN_STATICS_DISTRIBUTION_ID = 'mycloudfrontdistributionid' 29 | 30 | @task 31 | def create(ctx, env=DEFAULT_SERVER_ENV_NAME, app=DEFAULT_SERVER_APP_NAME): 32 | os.chdir('server') 33 | ctx.run('eb init -p "{ami}" {region} {profile} {name}'.format(region=REGION_OPT, 34 | ami=SERVER_AMI, 35 | profile=PROFILE_OPT, 36 | name=app)) 37 | 38 | # basic = '--timeout 30 --instance_type t2.micro --service-role aws-elasticbeanstalk-service-role' 39 | basic = '--timeout 30 --instance_type {0}'.format(SERVER_INSTANCE_TYPE) 40 | ctx.run("eb create {basic} {db} {region} {profile} -c {cname} {name}".format(basic=basic, 41 | db=DB_CMD, 42 | region=REGION_OPT, 43 | profile=PROFILE_OPT, 44 | cname=env, 45 | name=env)) 46 | 47 | @task 48 | def deploy(ctx, type='server'): 49 | if type == 'server': 50 | # Just for Server, we need to execute gulp first 51 | # Will deploy everything under /staticfiles. If new 52 | # third party packages are added, a local python manage.py collectstatic 53 | # will have to be run to move static files for that package to /staticfiles 54 | 55 | ctx.run('gulp deploy') 56 | os.chdir('server') 57 | ctx.run('eb deploy --region={region}'.format(region=AWS_REGION)) 58 | 59 | 60 | @task 61 | def ssh(ctx, type='server'): 62 | os.chdir('server') 63 | ctx.run('eb ssh') 64 | 65 | 66 | @task 67 | def build_statics(ctx, build=False): 68 | """Deploy static 69 | e.g. 70 | inv build-statics 71 | """ 72 | cmd = 'sh build-webapp.sh' 73 | ctx.run(cmd, pty=True) 74 | run_local(ctx, action='collectstatic') 75 | 76 | 77 | @task 78 | def deploy_statics(ctx, build=False): 79 | """Deploy static 80 | e.g. 81 | inv deploy-statics 82 | """ 83 | cmds = [ 84 | f'aws s3 sync --profile {AWS_PROFILE} ./staticfiles/ s3://{PROJECT_NAME}-statics/static', 85 | f'aws cloudfront --profile {AWS_PROFILE} create-invalidation --distribution-id {CDN_STATICS_DISTRIBUTION_ID} --paths /', 86 | ] 87 | 88 | for cmd in cmds: 89 | ctx.run(cmd, pty=True) 90 | 91 | 92 | @task 93 | def test(ctx, action='custom', path='./apps/'): 94 | """Full unit test and test coverage. 95 | Includes all django management funcions to setup databases 96 | (See runtest.sh) 97 | Args: 98 | action (string): One of 99 | - signoff: To run full/default runtest.sh 100 | - custom: To run a specific set of tests 101 | - stop: to stop all containters 102 | - down: to bring down (kill) all containers (and dbs) 103 | path (strin): If custom, path indicatest the test path to run 104 | e.g. 105 | inv test -a signoff # To run full default signoff test 106 | inv test -p ./apps/main # to run Report tests 107 | inv test -a stop # To stop all containers 108 | inv test -a down # To kill all containers 109 | """ 110 | # 2 Scale up or down 111 | cmd = f'docker-compose -f docker-compose.utest.yml -p {PROJECT_NAME.lower()}_test' 112 | if action == 'signoff': 113 | cmd += ' run --rm web' 114 | elif action == 'custom': 115 | cmd += f' run --rm web py.test -s {path}' 116 | elif action in ['stop', 'down', 'build',]: 117 | cmd += f' {action}' 118 | elif 'migrate' in action: 119 | cmd += ' run --rm web python manage.py migrate' 120 | else: 121 | print('action can only be signoff/custom/build/stop/down/migrate') 122 | ctx.run(cmd, pty=True) 123 | 124 | 125 | @task 126 | def run_local(ctx, action='up'): 127 | """To run local server 128 | Args: 129 | action (string): One of 130 | - up: To run docker-compose up 131 | - stop: to stop all containters 132 | - down: to bring down (kill) all containers (and dbs) 133 | - logs-: to show logs where is server, worker1, worker2, etc. 134 | e.g. 135 | inv run-local -a up # To run docker-compose up -d 136 | inv run-local -a stop # To run docker-compose stop 137 | inv run-local -a down # To run docker-compose down 138 | inv run-local -a logs-server # To show logs for Server 139 | inv run-local -a makemigrations # Run Django makemigrations 140 | inv run-local -a collectstatic # Run Django collectstatic 141 | """ 142 | # 2 Scale up or down 143 | cmd = f'docker-compose -f docker-compose.yml -p {PROJECT_NAME.lower()}' 144 | if action == 'up': 145 | cmd += ' up -d' 146 | elif action in ['stop', 'down', 'build']: 147 | cmd += f' {action}' 148 | elif 'logs' in action: 149 | parts = action.split('-') 150 | assert len(parts) == 2 151 | cmd += ' logs {}'.format(parts[1]) 152 | elif 'exec' in action: 153 | parts = action.split('-') 154 | assert len(parts) == 2 155 | cmd += ' exec {} bash'.format(parts[1]) 156 | elif 'makemigrations' in action: 157 | cmd += ' run --rm web python manage.py makemigrations' 158 | elif 'collectstatic' in action: 159 | cmd += ' run --rm web python manage.py collectstatic --noinput' 160 | elif 'migrate' in action: 161 | cmd += ' run --rm web python manage.py migrate' 162 | elif 'init' in action: 163 | cmd += ' run --rm web ./server-init.sh' 164 | else: 165 | print('action can only be up/stop/down') 166 | ctx.run(cmd, pty=True) 167 | -------------------------------------------------------------------------------- /webapp/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /webapp/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # change these settings to your own preference 11 | indent_style = space 12 | indent_size = 4 13 | 14 | # we recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | 23 | [{package,bower}.json] 24 | indent_style = space 25 | indent_size = 4 26 | -------------------------------------------------------------------------------- /webapp/.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /webapp/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .tmp 4 | .sass-cache 5 | -------------------------------------------------------------------------------- /webapp/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10 2 | # ========================================================== 3 | # Docker Image used for Building Gulp based systems 4 | # Usage: 5 | # docker build -t builder . 6 | # docker run --rm -v ${PWD}/webapp:/var/app/webapp -t builder 7 | # docker run --rm -v ${PWD}/webapp:/var/app/webapp --entrypoint npm -t builder install 8 | # docker run --rm -v ${PWD}/webapp:/var/app/webapp -t builder templates 9 | # ========================================================== 10 | 11 | RUN mkdir -p /var/app/webapp 12 | RUN mkdir -p /var/app/staticfiles 13 | RUN mkdir -p /var/app/server 14 | 15 | # Install app dependencies 16 | RUN npm install -g gulp-cli@2.3.0 17 | 18 | WORKDIR /var/app/webapp 19 | 20 | # Build Locally 21 | WORKDIR /var/app/webapp 22 | -------------------------------------------------------------------------------- /webapp/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{ project_name }}", 3 | "private": true, 4 | "dependencies": { 5 | "bootstrap-sass": "~3.3.5", 6 | "modernizr": "~2.8.1" 7 | }, 8 | "overrides": { 9 | "bootstrap-sass": { 10 | "main": [ 11 | "assets/stylesheets/_bootstrap.scss", 12 | "assets/fonts/bootstrap/*", 13 | "assets/javascripts/bootstrap.js" 14 | ] 15 | } 16 | }, 17 | "devDependencies": { 18 | "chai": "^3.5.0", 19 | "mocha": "^2.4.5" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /webapp/gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | // generated on 2016-03-23 using generator-webapp 2.0.0 2 | import gulp from 'gulp'; 3 | import del from 'del'; 4 | import {stream as wiredep} from 'wiredep'; 5 | import replace from 'gulp-replace'; 6 | var gulpLoadPlugins = require('gulp-load-plugins'); 7 | 8 | const $ = gulpLoadPlugins(); 9 | 10 | const config = { 11 | htmlFiles: 'src/*.html', 12 | fontFiles: 'src/app/fonts/**/*', 13 | scssFiles: 'src/app/styles/*.scss', 14 | jsFiles: 'src/app/scripts/**/*.js', 15 | jsTestFiles: 'test/spec/**/*.js', 16 | otherFiles: 'src/app/*.*', 17 | dist: '../staticfiles/dist/webapp', 18 | otherDist: '../staticfiles', 19 | templates: '../server/templates/dist/webapp' 20 | }; 21 | 22 | gulp.task('styles', () => { 23 | return gulp.src(config.scssFiles) 24 | .pipe($.plumber()) 25 | .pipe($.sourcemaps.init()) 26 | .pipe($.sass.sync({ 27 | outputStyle: 'expanded', 28 | precision: 10, 29 | includePaths: ['.'] 30 | }).on('error', $.sass.logError)) 31 | .pipe($.autoprefixer({browsers: ['> 1%', 'last 2 versions', 'Firefox ESR']})) 32 | .pipe($.sourcemaps.write()) 33 | .pipe(gulp.dest('.tmp/app/styles')); 34 | }); 35 | 36 | gulp.task('scripts', () => { 37 | return gulp.src(config.jsFiles) 38 | .pipe($.plumber()) 39 | .pipe($.sourcemaps.init()) 40 | .pipe($.babel()) 41 | .pipe($.sourcemaps.write('.')) 42 | .pipe(gulp.dest('.tmp/app/scripts')); 43 | }); 44 | 45 | gulp.task('html', ['styles', 'scripts'], () => { 46 | return gulp.src(config.htmlFiles) 47 | .pipe($.useref({searchPath: ['.tmp', 'src', '.']})) 48 | .pipe($.if('*.js', $.uglify())) 49 | .pipe($.if('*.css', $.cssnano())) 50 | .pipe($.if('*.js', $.rev())) 51 | .pipe($.if('*.css', $.rev())) 52 | .pipe($.revReplace()) 53 | .pipe($.if('*.html', $.htmlmin({collapseWhitespace: false}))) 54 | .pipe(gulp.dest(config.dist)) 55 | .pipe($.rev.manifest()) 56 | .pipe(gulp.dest(config.dist)); 57 | }); 58 | 59 | 60 | gulp.task('fonts', () => { 61 | return gulp.src(require('npmfiles')('**/*.{eot,svg,ttf,woff,woff2}', function (err) {}) 62 | .concat(config.fontFiles)) 63 | .pipe(gulp.dest('.tmp/app/fonts')) 64 | .pipe(gulp.dest(config.dist + '/app/fonts')); 65 | }); 66 | 67 | gulp.task('extras', () => { 68 | return gulp.src([ 69 | 'src/app/*.*' 70 | ], { 71 | dot: true 72 | }).pipe(gulp.dest(config.dist + '/app/extras')); 73 | }); 74 | 75 | gulp.task('clean', del.bind(null, ['.tmp'])); 76 | 77 | // inject NPM components 78 | gulp.task('wiredep', () => { 79 | gulp.src(config.scssFiles) 80 | .pipe(wiredep({ 81 | ignorePath: /^(\.\.\/)+/ 82 | })) 83 | .pipe(gulp.dest('src/app/styles')); 84 | 85 | gulp.src(config.htmlFiles) 86 | .pipe(wiredep({ 87 | exclude: ['bootstrap-sass'], 88 | ignorePath: /^(\.\.\/)*\.\./ 89 | })) 90 | .pipe(gulp.dest('src')); 91 | }); 92 | 93 | gulp.task('other', () => { 94 | return gulp.src(config.otherFiles) 95 | .pipe(gulp.dest(config.otherDist)); 96 | }); 97 | 98 | gulp.task('build', ['html', 'fonts', 'extras'], () => { 99 | return gulp.src(config.dist + '/**/*').pipe($.size({title: 'build', gzip: true})); 100 | }); 101 | 102 | gulp.task('templates', ['build'], () => { 103 | // Black Magic to convert all static references to use django's 'static' templatetags 104 | return gulp.src(config.dist + '/*.html') 105 | .pipe(replace(/href="app([/]\S*)"/g, 'href="{% static \'dist/webapp/app$1\' %}"')) 106 | .pipe(replace(/src="app([/]\S*)"/g, 'src="{% static \'dist/webapp/app$1\' %}"')) 107 | .pipe(gulp.dest(config.templates)); 108 | }); 109 | 110 | gulp.task('default', ['clean'], () => { 111 | gulp.start('templates'); 112 | }); 113 | -------------------------------------------------------------------------------- /webapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "engines": { 4 | "node": ">=10" 5 | }, 6 | "devDependencies": { 7 | "babel-core": "^6.26.0", 8 | "babel-preset-env": "^1.6.0", 9 | "babel-register": "^6.26.0", 10 | "chai": "^3.5.0", 11 | "del": "2.2.0", 12 | "graceful-fs": "4.1.11", 13 | "gulp": "3.9.1", 14 | "gulp-autoprefixer": "3.0.1", 15 | "gulp-babel": "^6.1.2", 16 | "gulp-cache": "0.4.2", 17 | "gulp-cssnano": "2.1.2", 18 | "gulp-htmlmin": "3.0.0", 19 | "gulp-if": "2.0.2", 20 | "gulp-load-plugins": "0.10.0", 21 | "gulp-plumber": "1.0.1", 22 | "gulp-replace": "0.5.4", 23 | "gulp-rev": "7.0.0", 24 | "gulp-rev-replace": "0.4.3", 25 | "gulp-sass": "2.0.0", 26 | "gulp-size": "1.2.1", 27 | "gulp-sourcemaps": "1.5.0", 28 | "gulp-uglify": "1.5.4", 29 | "gulp-useref": "3.1.2", 30 | "mocha": "^2.4.5", 31 | "npmfiles": "^0.1.3", 32 | "wiredep": "2.2.2" 33 | }, 34 | "dependencies": { 35 | "animate.css": "^3.5.2", 36 | "bootstrap-daterangepicker": "^2.1.24", 37 | "bootstrap-sass": "^3.3.6", 38 | "font-awesome": "^4.7.0", 39 | "moment": "^2.17.1" 40 | }, 41 | "overrides": { 42 | "bootstrap-sass": { 43 | "main": [ 44 | "assets/stylesheets/_bootstrap.scss", 45 | "assets/fonts/bootstrap/*", 46 | "assets/javascripts/bootstrap.js" 47 | ] 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /webapp/src/app/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkarchmer/django-aws-template/2bf15740cdad0ff9e6656c3c452a5ee6042cc0a6/webapp/src/app/apple-touch-icon.png -------------------------------------------------------------------------------- /webapp/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkarchmer/django-aws-template/2bf15740cdad0ff9e6656c3c452a5ee6042cc0a6/webapp/src/app/favicon.ico -------------------------------------------------------------------------------- /webapp/src/app/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org/ 2 | 3 | User-agent: * 4 | Disallow: 5 | -------------------------------------------------------------------------------- /webapp/src/app/scripts/main.js: -------------------------------------------------------------------------------- 1 | console.log('\'Allo \'Allo!'); 2 | -------------------------------------------------------------------------------- /webapp/src/app/styles/main.scss: -------------------------------------------------------------------------------- 1 | $icon-font-path: '../fonts/'; 2 | 3 | // bower:scss 4 | @import "node_modules/bootstrap-sass/assets/stylesheets/_bootstrap.scss"; 5 | // endbower 6 | 7 | .browserupgrade { 8 | margin: 0.2em 0; 9 | background: #ccc; 10 | color: #000; 11 | padding: 0.2em 0; 12 | } 13 | 14 | /* Space out content a bit */ 15 | body { 16 | padding-top: 20px; 17 | padding-bottom: 20px; 18 | } 19 | 20 | /* Everything but the jumbotron gets side spacing for mobile first views */ 21 | .header, 22 | .marketing, 23 | .footer { 24 | padding-left: 15px; 25 | padding-right: 15px; 26 | } 27 | 28 | /* Custom page header */ 29 | .header { 30 | border-bottom: 1px solid #e5e5e5; 31 | 32 | /* Make the masthead heading the same height as the navigation */ 33 | h3 { 34 | margin-top: 0; 35 | margin-bottom: 0; 36 | line-height: 40px; 37 | padding-bottom: 19px; 38 | } 39 | } 40 | 41 | /* Custom page footer */ 42 | .footer { 43 | padding-top: 19px; 44 | color: #777; 45 | border-top: 1px solid #e5e5e5; 46 | } 47 | 48 | .container-narrow > hr { 49 | margin: 30px 0; 50 | } 51 | 52 | /* Main marketing message and sign up button */ 53 | .jumbotron { 54 | text-align: center; 55 | border-bottom: 1px solid #e5e5e5; 56 | .btn { 57 | font-size: 21px; 58 | padding: 14px 24px; 59 | } 60 | } 61 | 62 | /* Supporting marketing content */ 63 | .marketing { 64 | margin: 40px 0; 65 | p + h4 { 66 | margin-top: 28px; 67 | } 68 | } 69 | 70 | /* Responsive: Portrait tablets and up */ 71 | @media screen and (min-width: 768px) { 72 | .container { 73 | max-width: 730px; 74 | } 75 | 76 | /* Remove the padding we set earlier */ 77 | .header, 78 | .marketing, 79 | .footer { 80 | padding-left: 0; 81 | padding-right: 0; 82 | } 83 | 84 | /* Space out the masthead */ 85 | .header { 86 | margin-bottom: 30px; 87 | } 88 | 89 | /* Remove the bottom border on the jumbotron for visual effect */ 90 | .jumbotron { 91 | border-bottom: 0; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /webapp/src/index.html: -------------------------------------------------------------------------------- 1 | {% templatetag openblock %} load static {% templatetag closeblock %} 2 | {% templatetag openblock %} comment {% templatetag closeblock %} 3 | ======================================================= 4 | ======================================================= 5 | 6 | THIS FILE IS GENERATED from {{ project_name }}/staticbase/src/index.html 7 | 8 | ======================================================= 9 | ======================================================= 10 | {% templatetag openblock %} endcomment {% templatetag closeblock %} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {{ project_name }} 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {% templatetag openblock %} block media {% templatetag closeblock %} 33 | {% templatetag openblock %} endblock {% templatetag closeblock %} 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 44 | 45 | {% templatetag openblock %} block body {% templatetag closeblock %} 46 |
47 |
48 | 52 |

{{ project_name }}

53 |
54 | 55 |
56 |

'Allo, 'Allo!

57 |

Always a pleasure scaffolding your apps.

58 |

Splendid!

59 |
60 | 61 |
62 |
63 |

Template generated with django-aws-template

64 |

65 |
66 | 67 | 70 |
71 | {% templatetag openblock %} endblock {% templatetag closeblock %} 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | {% templatetag openblock %} block js {% templatetag closeblock %} 99 | {% templatetag openblock %} endblock {% templatetag closeblock %} 100 | 101 | 102 | -------------------------------------------------------------------------------- /webapp/test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mocha Spec Runner 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 18 | 19 | 20 | 21 | 22 | 23 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /webapp/test/spec/test.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | describe('Give it some context', function () { 5 | describe('maybe a bit more context here', function () { 6 | it('should run here few assertions', function () { 7 | 8 | }); 9 | }); 10 | }); 11 | })(); 12 | --------------------------------------------------------------------------------