├── .editorconfig ├── .gitignore ├── Dockerfile ├── README.md ├── accounts ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── tests.py ├── urls.py └── views.py ├── api_tester.py ├── db.sqlite3 ├── docker-compose.yml ├── manage.py ├── project ├── .env.sample ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py ├── views.py └── wsgi.py ├── requirements.txt ├── static ├── ashim-d-silva-things.jpg └── cf-brand.png ├── staticfiles ├── admin │ ├── css │ │ ├── autocomplete.css │ │ ├── base.css │ │ ├── changelists.css │ │ ├── dark_mode.css │ │ ├── dashboard.css │ │ ├── fonts.css │ │ ├── forms.css │ │ ├── login.css │ │ ├── nav_sidebar.css │ │ ├── responsive.css │ │ ├── responsive_rtl.css │ │ ├── rtl.css │ │ ├── vendor │ │ │ └── select2 │ │ │ │ ├── LICENSE-SELECT2.md │ │ │ │ ├── select2.css │ │ │ │ └── select2.min.css │ │ └── widgets.css │ ├── fonts │ │ ├── LICENSE.txt │ │ ├── README.txt │ │ ├── Roboto-Bold-webfont.woff │ │ ├── Roboto-Light-webfont.woff │ │ └── Roboto-Regular-webfont.woff │ ├── img │ │ ├── LICENSE │ │ ├── README.txt │ │ ├── calendar-icons.svg │ │ ├── gis │ │ │ ├── move_vertex_off.svg │ │ │ └── move_vertex_on.svg │ │ ├── icon-addlink.svg │ │ ├── icon-alert.svg │ │ ├── icon-calendar.svg │ │ ├── icon-changelink.svg │ │ ├── icon-clock.svg │ │ ├── icon-deletelink.svg │ │ ├── icon-no.svg │ │ ├── icon-unknown-alt.svg │ │ ├── icon-unknown.svg │ │ ├── icon-viewlink.svg │ │ ├── icon-yes.svg │ │ ├── inline-delete.svg │ │ ├── search.svg │ │ ├── selector-icons.svg │ │ ├── sorting-icons.svg │ │ ├── tooltag-add.svg │ │ └── tooltag-arrowright.svg │ └── js │ │ ├── SelectBox.js │ │ ├── SelectFilter2.js │ │ ├── actions.js │ │ ├── admin │ │ ├── DateTimeShortcuts.js │ │ └── RelatedObjectLookups.js │ │ ├── autocomplete.js │ │ ├── calendar.js │ │ ├── cancel.js │ │ ├── change_form.js │ │ ├── collapse.js │ │ ├── core.js │ │ ├── filters.js │ │ ├── inlines.js │ │ ├── jquery.init.js │ │ ├── nav_sidebar.js │ │ ├── popup_response.js │ │ ├── prepopulate.js │ │ ├── prepopulate_init.js │ │ ├── urlify.js │ │ └── vendor │ │ ├── jquery │ │ ├── LICENSE.txt │ │ ├── jquery.js │ │ └── jquery.min.js │ │ ├── select2 │ │ ├── LICENSE.md │ │ ├── i18n │ │ │ ├── af.js │ │ │ ├── ar.js │ │ │ ├── az.js │ │ │ ├── bg.js │ │ │ ├── bn.js │ │ │ ├── bs.js │ │ │ ├── ca.js │ │ │ ├── cs.js │ │ │ ├── da.js │ │ │ ├── de.js │ │ │ ├── dsb.js │ │ │ ├── el.js │ │ │ ├── en.js │ │ │ ├── es.js │ │ │ ├── et.js │ │ │ ├── eu.js │ │ │ ├── fa.js │ │ │ ├── fi.js │ │ │ ├── fr.js │ │ │ ├── gl.js │ │ │ ├── he.js │ │ │ ├── hi.js │ │ │ ├── hr.js │ │ │ ├── hsb.js │ │ │ ├── hu.js │ │ │ ├── hy.js │ │ │ ├── id.js │ │ │ ├── is.js │ │ │ ├── it.js │ │ │ ├── ja.js │ │ │ ├── ka.js │ │ │ ├── km.js │ │ │ ├── ko.js │ │ │ ├── lt.js │ │ │ ├── lv.js │ │ │ ├── mk.js │ │ │ ├── ms.js │ │ │ ├── nb.js │ │ │ ├── ne.js │ │ │ ├── nl.js │ │ │ ├── pl.js │ │ │ ├── ps.js │ │ │ ├── pt-BR.js │ │ │ ├── pt.js │ │ │ ├── ro.js │ │ │ ├── ru.js │ │ │ ├── sk.js │ │ │ ├── sl.js │ │ │ ├── sq.js │ │ │ ├── sr-Cyrl.js │ │ │ ├── sr.js │ │ │ ├── sv.js │ │ │ ├── th.js │ │ │ ├── tk.js │ │ │ ├── tr.js │ │ │ ├── uk.js │ │ │ ├── vi.js │ │ │ ├── zh-CN.js │ │ │ └── zh-TW.js │ │ ├── select2.full.js │ │ └── select2.full.min.js │ │ └── xregexp │ │ ├── LICENSE.txt │ │ ├── xregexp.js │ │ └── xregexp.min.js ├── ashim-d-silva-Kw_zQBAChws-unsplash.jpg ├── cf-brand.png └── rest_framework │ ├── css │ ├── bootstrap-theme.min.css │ ├── bootstrap-theme.min.css.map │ ├── bootstrap-tweaks.css │ ├── bootstrap.min.css │ ├── bootstrap.min.css.map │ ├── default.css │ ├── font-awesome-4.0.3.css │ └── prettify.css │ ├── docs │ ├── css │ │ ├── base.css │ │ ├── highlight.css │ │ └── jquery.json-view.min.css │ ├── img │ │ ├── favicon.ico │ │ └── grid.png │ └── js │ │ ├── api.js │ │ ├── highlight.pack.js │ │ └── jquery.json-view.min.js │ ├── fonts │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 │ ├── img │ ├── glyphicons-halflings-white.png │ ├── glyphicons-halflings.png │ └── grid.png │ └── js │ ├── ajax-form.js │ ├── bootstrap.min.js │ ├── coreapi-0.1.1.js │ ├── csrf.js │ ├── default.js │ ├── jquery-3.5.1.min.js │ └── prettify-min.js ├── templates ├── about.html ├── base.html ├── home.html ├── registration │ ├── login.html │ └── signup.html └── things │ ├── thing_create.html │ ├── thing_delete.html │ ├── thing_detail.html │ ├── thing_list.html │ └── thing_update.html ├── things ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ └── __init__.py ├── models.py ├── permissions.py ├── serializers.py ├── tests.py ├── urls.py ├── urls_front.py ├── views.py └── views_front.py └── vercel.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | end_of_line = lf 11 | charset = utf-8 12 | 13 | # Docstrings and comments use max_line_length = 79 14 | [*.py] 15 | max_line_length = 79 16 | 17 | # Use 2 spaces for the HTML files 18 | [*.{html, js, css, yml, jinja2, json}] 19 | indent_size = 2 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Source: https://github.com/github/gitignore/blob/master/Python.gitignore 2 | 3 | # User generated 4 | ENV/ 5 | .vscode 6 | .idea 7 | .DS_Store 8 | .history 9 | 10 | 11 | # Byte-compiled / optimized / DLL files 12 | __pycache__/ 13 | *.py[cod] 14 | *$py.class 15 | .pytest_cache/ 16 | 17 | # C extensions 18 | *.so 19 | 20 | # Distribution / packaging 21 | .Python 22 | build/ 23 | develop-eggs/ 24 | dist/ 25 | downloads/ 26 | eggs/ 27 | .eggs/ 28 | lib/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | wheels/ 34 | *.egg-info/ 35 | .installed.cfg 36 | *.egg 37 | MANIFEST 38 | 39 | # PyInstaller 40 | # Usually these files are written by a python script from a template 41 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 42 | *.manifest 43 | *.spec 44 | 45 | # Installer logs 46 | pip-log.txt 47 | pip-delete-this-directory.txt 48 | 49 | # Unit test / coverage reports 50 | htmlcov/ 51 | .tox/ 52 | .coverage 53 | .coverage.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | *.cover 58 | .hypothesis/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | .static_storage/ 67 | .media/ 68 | local_settings.py 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # celery beat schedule file 90 | celerybeat-schedule 91 | 92 | # SageMath parsed files 93 | *.sage.py 94 | 95 | # Environments 96 | .env 97 | .venv 98 | env/ 99 | venv/ 100 | ENV/ 101 | env.bak/ 102 | venv.bak/ 103 | 104 | # Spyder project settings 105 | .spyderproject 106 | .spyproject 107 | 108 | # Rope project settings 109 | .ropeproject 110 | 111 | # mkdocs documentation 112 | /site 113 | 114 | # mypy 115 | .mypy_cache/ 116 | 117 | # js 118 | node_modules/ 119 | .next/ 120 | package-lock.json 121 | static/CACHE/ 122 | 123 | # poetry 124 | poetry.lock 125 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Python version 2 | FROM python:3 3 | 4 | # Set environment variables 5 | ENV PYTHONDONTWRITEBYTECODE 1 6 | ENV PYTHONUNBUFFERED 1 7 | 8 | # Set work directory 9 | WORKDIR /code 10 | 11 | # Install dependencies 12 | COPY requirements.txt /code/ 13 | RUN pip install -r requirements.txt 14 | 15 | # Copy project 16 | COPY . /code/ 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # api-quick-start 2 | 3 | Template Project for starting up CRUD API with Django Rest Framework 4 | 5 | ## Customization Steps 6 | 7 | - DO NOT migrate yet 8 | - add additional dependencies as needed 9 | - Re-export requirements.txt as needed 10 | - change `things` folder to the app name of your choice 11 | - Search through entire code base for `Thing`,`Things` and `things` to modify code to use your resource 12 | - `project/settings.py` 13 | - `project/urls.py` 14 | - App's files 15 | - `views.py` 16 | - `urls.py` 17 | - `admin.py` 18 | - `serializers.py` 19 | - `permissions.py` 20 | - "Front" files 21 | - if including a customer facing portion of the site then update/recreate: 22 | - `urls_front.py` 23 | - `views_front.py` 24 | - template files 25 | - Make sure to update project `urls.py` to add routes to the "front". 26 | - Update ThingModel with fields you need 27 | - Make sure to update other modules that would be affected by Model customizations. E.g. serializers, tests, etc. 28 | - Rename `project/.env.sample` to `.env` and update as needed 29 | - To generate secret key use `python3 -c "import secrets; print(secrets.token_urlsafe())"` 30 | - Run makemigrations and migrate commands when ready. 31 | - Run `python manage.py collectstatic` 32 | - This repository includes static assets in repository. If you are using a Content Delivery Network then remove `staticfiles` from repository. 33 | - Optional: Update `api_tester.py` 34 | 35 | ## Database 36 | 37 | **NOTE:** If you are using Postgres instead of SQLite then make sure to install `psycopg2-binary` and include in `requirements.txt` 38 | -------------------------------------------------------------------------------- /accounts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefellows/python-401-api-quickstart/aa02dd40ebaf766ed53cc41ed68cb70c2afd5e63/accounts/__init__.py -------------------------------------------------------------------------------- /accounts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | 4 | from .forms import CustomUserCreationForm, CustomUserChangeForm 5 | from .models import CustomUser 6 | 7 | 8 | class CustomUserAdmin(UserAdmin): 9 | add_form = CustomUserCreationForm 10 | form = CustomUserChangeForm 11 | model = CustomUser 12 | list_display = [ 13 | "email", 14 | "username", 15 | ] 16 | 17 | 18 | admin.site.register(CustomUser, CustomUserAdmin) 19 | -------------------------------------------------------------------------------- /accounts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountsConfig(AppConfig): 5 | name = "accounts" 6 | -------------------------------------------------------------------------------- /accounts/forms.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.forms import UserCreationForm, UserChangeForm 2 | from .models import CustomUser 3 | 4 | 5 | class CustomUserCreationForm(UserCreationForm): 6 | class Meta: 7 | model = CustomUser 8 | fields = ("username", "email") 9 | 10 | 11 | class CustomUserChangeForm(UserChangeForm): 12 | class Meta: 13 | model = CustomUser 14 | fields = ("username", "email") 15 | -------------------------------------------------------------------------------- /accounts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.7 on 2021-03-15 03:58 2 | 3 | import django.contrib.auth.models 4 | import django.contrib.auth.validators 5 | from django.db import migrations, models 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ('auth', '0012_alter_user_first_name_max_length'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='CustomUser', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('password', models.CharField(max_length=128, verbose_name='password')), 23 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 24 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 25 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), 26 | ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), 27 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 28 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), 29 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 30 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 31 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 32 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), 33 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), 34 | ], 35 | options={ 36 | 'verbose_name': 'user', 37 | 'verbose_name_plural': 'users', 38 | 'abstract': False, 39 | }, 40 | managers=[ 41 | ('objects', django.contrib.auth.models.UserManager()), 42 | ], 43 | ), 44 | ] 45 | -------------------------------------------------------------------------------- /accounts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefellows/python-401-api-quickstart/aa02dd40ebaf766ed53cc41ed68cb70c2afd5e63/accounts/migrations/__init__.py -------------------------------------------------------------------------------- /accounts/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractUser 2 | 3 | 4 | class CustomUser(AbstractUser): 5 | pass 6 | # add additional fields in here 7 | 8 | def __str__(self): 9 | return self.username 10 | -------------------------------------------------------------------------------- /accounts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /accounts/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import SignUpView 4 | 5 | urlpatterns = [ 6 | path("signup/", SignUpView.as_view(), name="signup"), 7 | ] 8 | -------------------------------------------------------------------------------- /accounts/views.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse_lazy 2 | from django.views.generic.edit import CreateView 3 | 4 | from .forms import CustomUserCreationForm 5 | 6 | class SignUpView(CreateView): 7 | form_class = CustomUserCreationForm 8 | success_url = reverse_lazy("login") 9 | template_name = "registration/signup.html" 10 | -------------------------------------------------------------------------------- /api_tester.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | import fire 4 | import requests 5 | from dotenv import load_dotenv 6 | 7 | dotenv_path = Path("project/.env") 8 | load_dotenv(dotenv_path=dotenv_path) 9 | 10 | # NOTE: Adjust these settings as needed in project/.env 11 | API_HOST = os.getenv("TEST_API_HOST") or "http://localhost:8000" 12 | RESOURCE_URI = os.getenv("TEST_RESOURCE_URI") or "things" 13 | USERNAME = os.getenv("TEST_USERNAME") 14 | PASSWORD = os.getenv("TEST_PASSWORD") 15 | 16 | 17 | class ApiTester: 18 | """CLI for testing API 19 | Server must be running. 20 | WARNING: Database queries are performed on supplied database. 21 | So be extra careful and/or use a test database. 22 | """ 23 | 24 | def __init__(self, host=API_HOST): 25 | self.host = host 26 | 27 | def fetch_tokens(self): 28 | """Fetches access and refresh JWT tokens from api 29 | 30 | Returns: 31 | tuple: access,refresh 32 | """ 33 | 34 | token_url = f"{self.host}/api/token/" 35 | 36 | response = requests.post( 37 | token_url, json={"username": USERNAME, "password": PASSWORD} 38 | ) 39 | 40 | data = response.json() 41 | 42 | tokens = data["access"], data["refresh"] 43 | 44 | return tokens 45 | 46 | def get_all(self): 47 | """get list of all resources from api 48 | Usage: python api_tester.py get_all 49 | 50 | Returns: JSON 51 | """ 52 | access_token = self.fetch_tokens()[0] 53 | 54 | url = f"{self.host}/api/v1/{RESOURCE_URI}/" 55 | 56 | headers = { 57 | "Authorization": f"Bearer {access_token}", 58 | } 59 | 60 | response = requests.get(url, headers=headers) 61 | 62 | return response.json() or 'No resources' 63 | 64 | def get_one(self, id): 65 | """get 1 resource by id from api 66 | 67 | Usage: 68 | python api_tester.py get_one 1 69 | 70 | Returns: JSON 71 | """ 72 | access_token = self.fetch_tokens()[0] 73 | 74 | url = f"{self.host}/api/v1/{RESOURCE_URI}/{id}" 75 | 76 | headers = { 77 | "Authorization": f"Bearer {access_token}", 78 | } 79 | 80 | response = requests.get(url, headers=headers) 81 | 82 | return response.json() 83 | 84 | # TODO adjust parameter names to match API 85 | def create(self, name, description, owner): 86 | """creates a resource in api 87 | 88 | Usage: 89 | python api_tester.py create / 90 | --name=required --description=required --owner=required 91 | 92 | Returns: JSON 93 | """ 94 | 95 | access_token = self.fetch_tokens()[0] 96 | 97 | url = f"{self.host}/api/v1/{RESOURCE_URI}/" 98 | 99 | headers = { 100 | "Authorization": f"Bearer {access_token}", 101 | } 102 | 103 | data = { 104 | "name": name, 105 | "description": description, 106 | "owner": int(owner), 107 | } 108 | 109 | response = requests.post(url, json=data, headers=headers) 110 | 111 | return response.json() 112 | 113 | def update(self, id, name=None, description=None, owner=None): 114 | """updates a resource in api 115 | 116 | Usage: 117 | python api_tester.py update 1 / 118 | --name=optional --description=optional --owner=optional 119 | 120 | Returns: JSON 121 | """ 122 | 123 | access_token = self.fetch_tokens()[0] 124 | 125 | url = f"{self.host}/api/v1/{RESOURCE_URI}/{id}/" 126 | 127 | headers = { 128 | "Authorization": f"Bearer {access_token}", 129 | } 130 | 131 | original = self.get_one(id) 132 | 133 | data = { 134 | "name": name or original["name"], 135 | "description": description or original["description"], 136 | "owner": owner or original["owner"], 137 | } 138 | 139 | response = requests.put(url, json=data, headers=headers) 140 | 141 | return response.text 142 | 143 | def delete(self, id): 144 | """deletes a resource in api 145 | 146 | Usage: 147 | python api_tester.py delete 1 148 | 149 | Returns: Empty string if no error 150 | """ 151 | 152 | access_token = self.fetch_tokens()[0] 153 | 154 | url = f"{self.host}/api/v1/{RESOURCE_URI}/{id}/" 155 | 156 | headers = { 157 | "Authorization": f"Bearer {access_token}", 158 | } 159 | 160 | response = requests.delete(url, headers=headers) 161 | 162 | return response.text 163 | 164 | 165 | if __name__ == "__main__": 166 | fire.Fire(ApiTester) 167 | -------------------------------------------------------------------------------- /db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefellows/python-401-api-quickstart/aa02dd40ebaf766ed53cc41ed68cb70c2afd5e63/db.sqlite3 -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | web: 5 | build: . 6 | command: gunicorn project.wsgi:application --bind 0.0.0.0:8000 --workers 4 7 | volumes: 8 | - .:/code 9 | ports: 10 | - 8000:8000 11 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /project/.env.sample: -------------------------------------------------------------------------------- 1 | SECRET_KEY=put-real-secret-key-here 2 | DEBUG=True 3 | 4 | ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0 5 | ALLOW_ALL_ORIGINS=True 6 | 7 | #DATABASE_ENGINE=django.db.backends.postgresql 8 | #DATABASE_NAME=postgres 9 | #DATABASE_USER=postgres 10 | #DATABASE_PASSWORD=put-real-db-password-here 11 | #DATABASE_HOST=db 12 | #DATABASE_PORT=5432 13 | -------------------------------------------------------------------------------- /project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefellows/python-401-api-quickstart/aa02dd40ebaf766ed53cc41ed68cb70c2afd5e63/project/__init__.py -------------------------------------------------------------------------------- /project/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for project project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /project/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for project project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.0.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.0/ref/settings/ 11 | """ 12 | 13 | from datetime import timedelta 14 | from pathlib import Path 15 | import environ 16 | 17 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 18 | BASE_DIR = Path(__file__).resolve().parent.parent 19 | 20 | env = environ.Env( 21 | DEBUG=(bool, False), 22 | ENVIRONMENT=(str, "PRODUCTION"), 23 | ALLOW_ALL_ORIGINS=(bool, False), 24 | ALLOWED_HOSTS=(list, []), 25 | ALLOWED_ORIGINS=(list, []), 26 | DATABASE_ENGINE=(str, "django.db.backends.sqlite3"), 27 | DATABASE_NAME=(str, BASE_DIR / "db.sqlite3"), 28 | DATABASE_USER=(str, ""), 29 | DATABASE_PASSWORD=(str, ""), 30 | DATABASE_HOST=(str, ""), 31 | DATABASE_PORT=(int, 5432), 32 | ) 33 | 34 | environ.Env.read_env() 35 | 36 | ENVIRONMENT = env.str("ENVIRONMENT") 37 | 38 | # Quick-start development settings - unsuitable for production 39 | # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ 40 | 41 | SECRET_KEY = env.str("SECRET_KEY") 42 | 43 | DEBUG = env.bool("DEBUG") 44 | 45 | ALLOWED_HOSTS = tuple(env.list("ALLOWED_HOSTS")) 46 | 47 | # Quick-start development settings - unsuitable for production 48 | # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ 49 | 50 | # Application definition 51 | 52 | INSTALLED_APPS = [ 53 | 'django.contrib.admin', 54 | 'django.contrib.auth', 55 | 'django.contrib.contenttypes', 56 | 'django.contrib.sessions', 57 | 'django.contrib.messages', 58 | 'django.contrib.staticfiles', 59 | # 3rd party 60 | "rest_framework", 61 | "corsheaders", 62 | # local 63 | "accounts", 64 | "things", 65 | ] 66 | 67 | MIDDLEWARE = [ 68 | 'django.middleware.security.SecurityMiddleware', 69 | "corsheaders.middleware.CorsMiddleware", 70 | "whitenoise.middleware.WhiteNoiseMiddleware", 71 | 'django.contrib.sessions.middleware.SessionMiddleware', 72 | 'django.middleware.common.CommonMiddleware', 73 | 'django.middleware.csrf.CsrfViewMiddleware', 74 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 75 | 'django.contrib.messages.middleware.MessageMiddleware', 76 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 77 | ] 78 | 79 | ROOT_URLCONF = 'project.urls' 80 | 81 | TEMPLATES = [ 82 | { 83 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 84 | 'DIRS': [ 85 | BASE_DIR / 'templates', 86 | ], 87 | 'APP_DIRS': True, 88 | 'OPTIONS': { 89 | 'context_processors': [ 90 | 'django.template.context_processors.debug', 91 | 'django.template.context_processors.request', 92 | 'django.contrib.auth.context_processors.auth', 93 | 'django.contrib.messages.context_processors.messages', 94 | ], 95 | }, 96 | }, 97 | ] 98 | 99 | WSGI_APPLICATION = 'project.wsgi.application' 100 | 101 | # Database 102 | # https://docs.djangoproject.com/en/4.0/ref/settings/#databases 103 | 104 | DATABASES = { 105 | "default": { 106 | "ENGINE": env.str("DATABASE_ENGINE"), 107 | "NAME": env.str("DATABASE_NAME"), 108 | "USER": env.str("DATABASE_USER"), 109 | "PASSWORD": env.str("DATABASE_PASSWORD"), 110 | "HOST": env.str("DATABASE_HOST"), 111 | "PORT": env.int("DATABASE_PORT"), 112 | } 113 | } 114 | 115 | # Password validation 116 | # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators 117 | 118 | AUTH_PASSWORD_VALIDATORS = [ 119 | { 120 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 121 | }, 122 | { 123 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 124 | }, 125 | { 126 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 127 | }, 128 | { 129 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 130 | }, 131 | ] 132 | 133 | # Internationalization 134 | # https://docs.djangoproject.com/en/4.0/topics/i18n/ 135 | 136 | LANGUAGE_CODE = 'en-us' 137 | 138 | TIME_ZONE = 'UTC' 139 | 140 | USE_I18N = True 141 | 142 | USE_TZ = True 143 | 144 | # Static files (CSS, JavaScript, Images) 145 | # https://docs.djangoproject.com/en/4.0/howto/static-files/ 146 | 147 | STATIC_URL = 'static/' 148 | STATIC_ROOT = BASE_DIR / "staticfiles" 149 | 150 | # Default primary key field type 151 | # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field 152 | 153 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 154 | 155 | AUTH_USER_MODEL = "accounts.CustomUser" 156 | 157 | REST_FRAMEWORK = { 158 | "DEFAULT_PERMISSION_CLASSES": [ 159 | "rest_framework.permissions.IsAuthenticated", 160 | ], 161 | "DEFAULT_AUTHENTICATION_CLASSES": [ 162 | "rest_framework_simplejwt.authentication.JWTAuthentication", 163 | "rest_framework.authentication.SessionAuthentication", 164 | "rest_framework.authentication.BasicAuthentication", 165 | ], 166 | } 167 | 168 | SIMPLE_JWT = { 169 | "ACCESS_TOKEN_LIFETIME": timedelta( 170 | seconds=60 * 60 171 | ), # lasts for 60 minutes 172 | } 173 | 174 | CORS_ORIGIN_WHITELIST = tuple(env.list("ALLOWED_ORIGINS")) 175 | CORS_ALLOW_ALL_ORIGINS = env.bool("ALLOW_ALL_ORIGINS") 176 | 177 | # TAILWIND 178 | STATICFILES_DIRS = [ 179 | BASE_DIR / 'static' 180 | ] 181 | 182 | # AUTH 183 | LOGIN_REDIRECT_URL = "home" 184 | LOGOUT_REDIRECT_URL = "home" 185 | -------------------------------------------------------------------------------- /project/urls.py: -------------------------------------------------------------------------------- 1 | """project URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import include, path 18 | from rest_framework_simplejwt import views as jwt_views 19 | from .views import MyTokenObtainPairView 20 | from django.views.generic.base import TemplateView 21 | 22 | urlpatterns = [ 23 | path("admin/", admin.site.urls), 24 | path("api/v1/things/", include("things.urls")), 25 | path("api-auth/", include("rest_framework.urls")), 26 | path( 27 | "api/token/", 28 | MyTokenObtainPairView.as_view(), 29 | name="token_obtain_pair", 30 | ), 31 | path( 32 | "api/token/refresh/", 33 | jwt_views.TokenRefreshView.as_view(), 34 | name="token_refresh", 35 | ), 36 | path("things/", include("things.urls_front")), 37 | path("", TemplateView.as_view(template_name="home.html"), name="home"), 38 | path("about/", TemplateView.as_view(template_name="about.html"), name="about"), 39 | path("accounts/", include("accounts.urls")), 40 | path("accounts/", include("django.contrib.auth.urls")), 41 | ] 42 | -------------------------------------------------------------------------------- /project/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework_simplejwt.serializers import TokenObtainPairSerializer 2 | from rest_framework_simplejwt.views import TokenObtainPairView 3 | 4 | 5 | class MyTokenObtainPairSerializer(TokenObtainPairSerializer): 6 | @classmethod 7 | def get_token(cls, user): 8 | token = super().get_token(user) 9 | 10 | # Add custom claims 11 | token["email"] = user.email 12 | token["username"] = user.username 13 | # ... 14 | 15 | return token 16 | 17 | 18 | class MyTokenObtainPairView(TokenObtainPairView): 19 | serializer_class = MyTokenObtainPairSerializer 20 | -------------------------------------------------------------------------------- /project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for project 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/4.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') 15 | 16 | application = get_wsgi_application() 17 | 18 | # vercel deployment requires "app" 19 | app = application 20 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.6.0 2 | certifi==2022.12.7 3 | charset-normalizer==3.0.1 4 | Django==4.1.5 5 | django-appconf==1.0.5 6 | django-cors-headers==3.13.0 7 | django-environ==0.9.0 8 | djangorestframework==3.14.0 9 | djangorestframework-simplejwt==5.2.2 10 | fire==0.5.0 11 | gunicorn==20.1.0 12 | idna==3.4 13 | PyJWT==2.6.0 14 | pytz==2022.7.1 15 | rcssmin==1.1.1 16 | requests==2.28.2 17 | rjsmin==1.2.1 18 | six==1.16.0 19 | sqlparse==0.4.3 20 | termcolor==2.2.0 21 | urllib3==1.26.14 22 | whitenoise==6.3.0 23 | -------------------------------------------------------------------------------- /static/ashim-d-silva-things.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefellows/python-401-api-quickstart/aa02dd40ebaf766ed53cc41ed68cb70c2afd5e63/static/ashim-d-silva-things.jpg -------------------------------------------------------------------------------- /static/cf-brand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefellows/python-401-api-quickstart/aa02dd40ebaf766ed53cc41ed68cb70c2afd5e63/static/cf-brand.png -------------------------------------------------------------------------------- /staticfiles/admin/css/changelists.css: -------------------------------------------------------------------------------- 1 | /* CHANGELISTS */ 2 | 3 | #changelist { 4 | display: flex; 5 | align-items: flex-start; 6 | justify-content: space-between; 7 | } 8 | 9 | #changelist .changelist-form-container { 10 | flex: 1 1 auto; 11 | min-width: 0; 12 | } 13 | 14 | #changelist table { 15 | width: 100%; 16 | } 17 | 18 | .change-list .hiddenfields { display:none; } 19 | 20 | .change-list .filtered table { 21 | border-right: none; 22 | } 23 | 24 | .change-list .filtered { 25 | min-height: 400px; 26 | } 27 | 28 | .change-list .filtered .results, .change-list .filtered .paginator, 29 | .filtered #toolbar, .filtered div.xfull { 30 | width: auto; 31 | } 32 | 33 | .change-list .filtered table tbody th { 34 | padding-right: 1em; 35 | } 36 | 37 | #changelist-form .results { 38 | overflow-x: auto; 39 | width: 100%; 40 | } 41 | 42 | #changelist .toplinks { 43 | border-bottom: 1px solid var(--hairline-color); 44 | } 45 | 46 | #changelist .paginator { 47 | color: var(--body-quiet-color); 48 | border-bottom: 1px solid var(--hairline-color); 49 | background: var(--body-bg); 50 | overflow: hidden; 51 | } 52 | 53 | /* CHANGELIST TABLES */ 54 | 55 | #changelist table thead th { 56 | padding: 0; 57 | white-space: nowrap; 58 | vertical-align: middle; 59 | } 60 | 61 | #changelist table thead th.action-checkbox-column { 62 | width: 1.5em; 63 | text-align: center; 64 | } 65 | 66 | #changelist table tbody td.action-checkbox { 67 | text-align: center; 68 | } 69 | 70 | #changelist table tfoot { 71 | color: var(--body-quiet-color); 72 | } 73 | 74 | /* TOOLBAR */ 75 | 76 | #toolbar { 77 | padding: 8px 10px; 78 | margin-bottom: 15px; 79 | border-top: 1px solid var(--hairline-color); 80 | border-bottom: 1px solid var(--hairline-color); 81 | background: var(--darkened-bg); 82 | color: var(--body-quiet-color); 83 | } 84 | 85 | #toolbar form input { 86 | border-radius: 4px; 87 | font-size: 0.875rem; 88 | padding: 5px; 89 | color: var(--body-fg); 90 | } 91 | 92 | #toolbar #searchbar { 93 | height: 19px; 94 | border: 1px solid var(--border-color); 95 | padding: 2px 5px; 96 | margin: 0; 97 | vertical-align: top; 98 | font-size: 0.8125rem; 99 | max-width: 100%; 100 | } 101 | 102 | #toolbar #searchbar:focus { 103 | border-color: var(--body-quiet-color); 104 | } 105 | 106 | #toolbar form input[type="submit"] { 107 | border: 1px solid var(--border-color); 108 | font-size: 0.8125rem; 109 | padding: 4px 8px; 110 | margin: 0; 111 | vertical-align: middle; 112 | background: var(--body-bg); 113 | box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset; 114 | cursor: pointer; 115 | color: var(--body-fg); 116 | } 117 | 118 | #toolbar form input[type="submit"]:focus, 119 | #toolbar form input[type="submit"]:hover { 120 | border-color: var(--body-quiet-color); 121 | } 122 | 123 | #changelist-search img { 124 | vertical-align: middle; 125 | margin-right: 4px; 126 | } 127 | 128 | #changelist-search .help { 129 | word-break: break-word; 130 | } 131 | 132 | /* FILTER COLUMN */ 133 | 134 | #changelist-filter { 135 | flex: 0 0 240px; 136 | order: 1; 137 | background: var(--darkened-bg); 138 | border-left: none; 139 | margin: 0 0 0 30px; 140 | } 141 | 142 | #changelist-filter h2 { 143 | font-size: 0.875rem; 144 | text-transform: uppercase; 145 | letter-spacing: 0.5px; 146 | padding: 5px 15px; 147 | margin-bottom: 12px; 148 | border-bottom: none; 149 | } 150 | 151 | #changelist-filter h3, 152 | #changelist-filter details summary { 153 | font-weight: 400; 154 | padding: 0 15px; 155 | margin-bottom: 10px; 156 | } 157 | 158 | #changelist-filter details summary > * { 159 | display: inline; 160 | } 161 | 162 | #changelist-filter details > summary { 163 | list-style-type: none; 164 | } 165 | 166 | #changelist-filter details > summary::-webkit-details-marker { 167 | display: none; 168 | } 169 | 170 | #changelist-filter details > summary::before { 171 | content: '→'; 172 | font-weight: bold; 173 | color: var(--link-hover-color); 174 | } 175 | 176 | #changelist-filter details[open] > summary::before { 177 | content: '↓'; 178 | } 179 | 180 | #changelist-filter ul { 181 | margin: 5px 0; 182 | padding: 0 15px 15px; 183 | border-bottom: 1px solid var(--hairline-color); 184 | } 185 | 186 | #changelist-filter ul:last-child { 187 | border-bottom: none; 188 | } 189 | 190 | #changelist-filter li { 191 | list-style-type: none; 192 | margin-left: 0; 193 | padding-left: 0; 194 | } 195 | 196 | #changelist-filter a { 197 | display: block; 198 | color: var(--body-quiet-color); 199 | word-break: break-word; 200 | } 201 | 202 | #changelist-filter li.selected { 203 | border-left: 5px solid var(--hairline-color); 204 | padding-left: 10px; 205 | margin-left: -15px; 206 | } 207 | 208 | #changelist-filter li.selected a { 209 | color: var(--link-selected-fg); 210 | } 211 | 212 | #changelist-filter a:focus, #changelist-filter a:hover, 213 | #changelist-filter li.selected a:focus, 214 | #changelist-filter li.selected a:hover { 215 | color: var(--link-hover-color); 216 | } 217 | 218 | #changelist-filter #changelist-filter-clear a { 219 | font-size: 0.8125rem; 220 | padding-bottom: 10px; 221 | border-bottom: 1px solid var(--hairline-color); 222 | } 223 | 224 | /* DATE DRILLDOWN */ 225 | 226 | .change-list ul.toplinks { 227 | display: block; 228 | float: left; 229 | padding: 0; 230 | margin: 0; 231 | width: 100%; 232 | } 233 | 234 | .change-list ul.toplinks li { 235 | padding: 3px 6px; 236 | font-weight: bold; 237 | list-style-type: none; 238 | display: inline-block; 239 | } 240 | 241 | .change-list ul.toplinks .date-back a { 242 | color: var(--body-quiet-color); 243 | } 244 | 245 | .change-list ul.toplinks .date-back a:focus, 246 | .change-list ul.toplinks .date-back a:hover { 247 | color: var(--link-hover-color); 248 | } 249 | 250 | /* ACTIONS */ 251 | 252 | .filtered .actions { 253 | border-right: none; 254 | } 255 | 256 | #changelist table input { 257 | margin: 0; 258 | vertical-align: baseline; 259 | } 260 | 261 | #changelist table tbody tr.selected { 262 | background-color: var(--selected-row); 263 | } 264 | 265 | #changelist .actions { 266 | padding: 10px; 267 | background: var(--body-bg); 268 | border-top: none; 269 | border-bottom: none; 270 | line-height: 24px; 271 | color: var(--body-quiet-color); 272 | width: 100%; 273 | } 274 | 275 | #changelist .actions span.all, 276 | #changelist .actions span.action-counter, 277 | #changelist .actions span.clear, 278 | #changelist .actions span.question { 279 | font-size: 0.8125rem; 280 | margin: 0 0.5em; 281 | } 282 | 283 | #changelist .actions:last-child { 284 | border-bottom: none; 285 | } 286 | 287 | #changelist .actions select { 288 | vertical-align: top; 289 | height: 24px; 290 | color: var(--body-fg); 291 | border: 1px solid var(--border-color); 292 | border-radius: 4px; 293 | font-size: 0.875rem; 294 | padding: 0 0 0 4px; 295 | margin: 0; 296 | margin-left: 10px; 297 | } 298 | 299 | #changelist .actions select:focus { 300 | border-color: var(--body-quiet-color); 301 | } 302 | 303 | #changelist .actions label { 304 | display: inline-block; 305 | vertical-align: middle; 306 | font-size: 0.8125rem; 307 | } 308 | 309 | #changelist .actions .button { 310 | font-size: 0.8125rem; 311 | border: 1px solid var(--border-color); 312 | border-radius: 4px; 313 | background: var(--body-bg); 314 | box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset; 315 | cursor: pointer; 316 | height: 24px; 317 | line-height: 1; 318 | padding: 4px 8px; 319 | margin: 0; 320 | color: var(--body-fg); 321 | } 322 | 323 | #changelist .actions .button:focus, #changelist .actions .button:hover { 324 | border-color: var(--body-quiet-color); 325 | } 326 | -------------------------------------------------------------------------------- /staticfiles/admin/css/dark_mode.css: -------------------------------------------------------------------------------- 1 | @media (prefers-color-scheme: dark) { 2 | :root { 3 | --primary: #264b5d; 4 | --primary-fg: #f7f7f7; 5 | 6 | --body-fg: #eeeeee; 7 | --body-bg: #121212; 8 | --body-quiet-color: #e0e0e0; 9 | --body-loud-color: #ffffff; 10 | 11 | --breadcrumbs-link-fg: #e0e0e0; 12 | --breadcrumbs-bg: var(--primary); 13 | 14 | --link-fg: #81d4fa; 15 | --link-hover-color: #4ac1f7; 16 | --link-selected-fg: #6f94c6; 17 | 18 | --hairline-color: #272727; 19 | --border-color: #353535; 20 | 21 | --error-fg: #e35f5f; 22 | --message-success-bg: #006b1b; 23 | --message-warning-bg: #583305; 24 | --message-error-bg: #570808; 25 | 26 | --darkened-bg: #212121; 27 | --selected-bg: #1b1b1b; 28 | --selected-row: #00363a; 29 | 30 | --close-button-bg: #333333; 31 | --close-button-hover-bg: #666666; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /staticfiles/admin/css/dashboard.css: -------------------------------------------------------------------------------- 1 | /* DASHBOARD */ 2 | 3 | .dashboard .module table th { 4 | width: 100%; 5 | } 6 | 7 | .dashboard .module table td { 8 | white-space: nowrap; 9 | } 10 | 11 | .dashboard .module table td a { 12 | display: block; 13 | padding-right: .6em; 14 | } 15 | 16 | /* RECENT ACTIONS MODULE */ 17 | 18 | .module ul.actionlist { 19 | margin-left: 0; 20 | } 21 | 22 | ul.actionlist li { 23 | list-style-type: none; 24 | overflow: hidden; 25 | text-overflow: ellipsis; 26 | } 27 | -------------------------------------------------------------------------------- /staticfiles/admin/css/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Roboto'; 3 | src: url('../fonts/Roboto-Bold-webfont.woff'); 4 | font-weight: 700; 5 | font-style: normal; 6 | } 7 | 8 | @font-face { 9 | font-family: 'Roboto'; 10 | src: url('../fonts/Roboto-Regular-webfont.woff'); 11 | font-weight: 400; 12 | font-style: normal; 13 | } 14 | 15 | @font-face { 16 | font-family: 'Roboto'; 17 | src: url('../fonts/Roboto-Light-webfont.woff'); 18 | font-weight: 300; 19 | font-style: normal; 20 | } 21 | -------------------------------------------------------------------------------- /staticfiles/admin/css/login.css: -------------------------------------------------------------------------------- 1 | /* LOGIN FORM */ 2 | 3 | .login { 4 | background: var(--darkened-bg); 5 | height: auto; 6 | } 7 | 8 | .login #header { 9 | height: auto; 10 | padding: 15px 16px; 11 | justify-content: center; 12 | } 13 | 14 | .login #header h1 { 15 | font-size: 1.125rem; 16 | margin: 0; 17 | } 18 | 19 | .login #header h1 a { 20 | color: var(--header-link-color); 21 | } 22 | 23 | .login #content { 24 | padding: 20px 20px 0; 25 | } 26 | 27 | .login #container { 28 | background: var(--body-bg); 29 | border: 1px solid var(--hairline-color); 30 | border-radius: 4px; 31 | overflow: hidden; 32 | width: 28em; 33 | min-width: 300px; 34 | margin: 100px auto; 35 | height: auto; 36 | } 37 | 38 | .login .form-row { 39 | padding: 4px 0; 40 | } 41 | 42 | .login .form-row label { 43 | display: block; 44 | line-height: 2em; 45 | } 46 | 47 | .login .form-row #id_username, .login .form-row #id_password { 48 | padding: 8px; 49 | width: 100%; 50 | box-sizing: border-box; 51 | } 52 | 53 | .login .submit-row { 54 | padding: 1em 0 0 0; 55 | margin: 0; 56 | text-align: center; 57 | } 58 | 59 | .login .password-reset-link { 60 | text-align: center; 61 | } 62 | -------------------------------------------------------------------------------- /staticfiles/admin/css/nav_sidebar.css: -------------------------------------------------------------------------------- 1 | .sticky { 2 | position: sticky; 3 | top: 0; 4 | max-height: 100vh; 5 | } 6 | 7 | .toggle-nav-sidebar { 8 | z-index: 20; 9 | left: 0; 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | flex: 0 0 23px; 14 | width: 23px; 15 | border: 0; 16 | border-right: 1px solid var(--hairline-color); 17 | background-color: var(--body-bg); 18 | cursor: pointer; 19 | font-size: 1.25rem; 20 | color: var(--link-fg); 21 | padding: 0; 22 | } 23 | 24 | [dir="rtl"] .toggle-nav-sidebar { 25 | border-left: 1px solid var(--hairline-color); 26 | border-right: 0; 27 | } 28 | 29 | .toggle-nav-sidebar:hover, 30 | .toggle-nav-sidebar:focus { 31 | background-color: var(--darkened-bg); 32 | } 33 | 34 | #nav-sidebar { 35 | z-index: 15; 36 | flex: 0 0 275px; 37 | left: -276px; 38 | margin-left: -276px; 39 | border-top: 1px solid transparent; 40 | border-right: 1px solid var(--hairline-color); 41 | background-color: var(--body-bg); 42 | overflow: auto; 43 | } 44 | 45 | [dir="rtl"] #nav-sidebar { 46 | border-left: 1px solid var(--hairline-color); 47 | border-right: 0; 48 | left: 0; 49 | margin-left: 0; 50 | right: -276px; 51 | margin-right: -276px; 52 | } 53 | 54 | .toggle-nav-sidebar::before { 55 | content: '\00BB'; 56 | } 57 | 58 | .main.shifted .toggle-nav-sidebar::before { 59 | content: '\00AB'; 60 | } 61 | 62 | .main.shifted > #nav-sidebar { 63 | margin-left: 0; 64 | } 65 | 66 | [dir="rtl"] .main.shifted > #nav-sidebar { 67 | margin-right: 0; 68 | } 69 | 70 | #nav-sidebar .module th { 71 | width: 100%; 72 | overflow-wrap: anywhere; 73 | } 74 | 75 | #nav-sidebar .module th, 76 | #nav-sidebar .module caption { 77 | padding-left: 16px; 78 | } 79 | 80 | #nav-sidebar .module td { 81 | white-space: nowrap; 82 | } 83 | 84 | [dir="rtl"] #nav-sidebar .module th, 85 | [dir="rtl"] #nav-sidebar .module caption { 86 | padding-left: 8px; 87 | padding-right: 16px; 88 | } 89 | 90 | #nav-sidebar .current-app .section:link, 91 | #nav-sidebar .current-app .section:visited { 92 | color: var(--header-color); 93 | font-weight: bold; 94 | } 95 | 96 | #nav-sidebar .current-model { 97 | background: var(--selected-row); 98 | } 99 | 100 | .main > #nav-sidebar + .content { 101 | max-width: calc(100% - 23px); 102 | } 103 | 104 | .main.shifted > #nav-sidebar + .content { 105 | max-width: calc(100% - 299px); 106 | } 107 | 108 | @media (max-width: 767px) { 109 | #nav-sidebar, #toggle-nav-sidebar { 110 | display: none; 111 | } 112 | 113 | .main > #nav-sidebar + .content, 114 | .main.shifted > #nav-sidebar + .content { 115 | max-width: 100%; 116 | } 117 | } 118 | 119 | #nav-filter { 120 | width: 100%; 121 | box-sizing: border-box; 122 | padding: 2px 5px; 123 | margin: 5px 0; 124 | border: 1px solid var(--border-color); 125 | background-color: var(--darkened-bg); 126 | color: var(--body-fg); 127 | } 128 | 129 | #nav-filter:focus { 130 | border-color: var(--body-quiet-color); 131 | } 132 | 133 | #nav-filter.no-results { 134 | background: var(--message-error-bg); 135 | } 136 | 137 | #nav-sidebar table { 138 | width: 100%; 139 | } 140 | -------------------------------------------------------------------------------- /staticfiles/admin/css/responsive_rtl.css: -------------------------------------------------------------------------------- 1 | /* TABLETS */ 2 | 3 | @media (max-width: 1024px) { 4 | [dir="rtl"] .colMS { 5 | margin-right: 0; 6 | } 7 | 8 | [dir="rtl"] #user-tools { 9 | text-align: right; 10 | } 11 | 12 | [dir="rtl"] #changelist .actions label { 13 | padding-left: 10px; 14 | padding-right: 0; 15 | } 16 | 17 | [dir="rtl"] #changelist .actions select { 18 | margin-left: 0; 19 | margin-right: 15px; 20 | } 21 | 22 | [dir="rtl"] .change-list .filtered .results, 23 | [dir="rtl"] .change-list .filtered .paginator, 24 | [dir="rtl"] .filtered #toolbar, 25 | [dir="rtl"] .filtered div.xfull, 26 | [dir="rtl"] .filtered .actions, 27 | [dir="rtl"] #changelist-filter { 28 | margin-left: 0; 29 | } 30 | 31 | [dir="rtl"] .inline-group ul.tools a.add, 32 | [dir="rtl"] .inline-group div.add-row a, 33 | [dir="rtl"] .inline-group .tabular tr.add-row td a { 34 | padding: 8px 26px 8px 10px; 35 | background-position: calc(100% - 8px) 9px; 36 | } 37 | 38 | [dir="rtl"] .related-widget-wrapper-link + .selector { 39 | margin-right: 0; 40 | margin-left: 15px; 41 | } 42 | 43 | [dir="rtl"] .selector .selector-filter label { 44 | margin-right: 0; 45 | margin-left: 8px; 46 | } 47 | 48 | [dir="rtl"] .object-tools li { 49 | float: right; 50 | } 51 | 52 | [dir="rtl"] .object-tools li + li { 53 | margin-left: 0; 54 | margin-right: 15px; 55 | } 56 | 57 | [dir="rtl"] .dashboard .module table td a { 58 | padding-left: 0; 59 | padding-right: 16px; 60 | } 61 | } 62 | 63 | /* MOBILE */ 64 | 65 | @media (max-width: 767px) { 66 | [dir="rtl"] .aligned .related-lookup, 67 | [dir="rtl"] .aligned .datetimeshortcuts { 68 | margin-left: 0; 69 | margin-right: 15px; 70 | } 71 | 72 | [dir="rtl"] .aligned ul { 73 | margin-right: 0; 74 | } 75 | 76 | [dir="rtl"] #changelist-filter { 77 | margin-left: 0; 78 | margin-right: 0; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /staticfiles/admin/css/rtl.css: -------------------------------------------------------------------------------- 1 | /* GLOBAL */ 2 | 3 | th { 4 | text-align: right; 5 | } 6 | 7 | .module h2, .module caption { 8 | text-align: right; 9 | } 10 | 11 | .module ul, .module ol { 12 | margin-left: 0; 13 | margin-right: 1.5em; 14 | } 15 | 16 | .viewlink, .addlink, .changelink { 17 | padding-left: 0; 18 | padding-right: 16px; 19 | background-position: 100% 1px; 20 | } 21 | 22 | .deletelink { 23 | padding-left: 0; 24 | padding-right: 16px; 25 | background-position: 100% 1px; 26 | } 27 | 28 | .object-tools { 29 | float: left; 30 | } 31 | 32 | thead th:first-child, 33 | tfoot td:first-child { 34 | border-left: none; 35 | } 36 | 37 | /* LAYOUT */ 38 | 39 | #user-tools { 40 | right: auto; 41 | left: 0; 42 | text-align: left; 43 | } 44 | 45 | div.breadcrumbs { 46 | text-align: right; 47 | } 48 | 49 | #content-main { 50 | float: right; 51 | } 52 | 53 | #content-related { 54 | float: left; 55 | margin-left: -300px; 56 | margin-right: auto; 57 | } 58 | 59 | .colMS { 60 | margin-left: 300px; 61 | margin-right: 0; 62 | } 63 | 64 | /* SORTABLE TABLES */ 65 | 66 | table thead th.sorted .sortoptions { 67 | float: left; 68 | } 69 | 70 | thead th.sorted .text { 71 | padding-right: 0; 72 | padding-left: 42px; 73 | } 74 | 75 | /* dashboard styles */ 76 | 77 | .dashboard .module table td a { 78 | padding-left: .6em; 79 | padding-right: 16px; 80 | } 81 | 82 | /* changelists styles */ 83 | 84 | .change-list .filtered table { 85 | border-left: none; 86 | border-right: 0px none; 87 | } 88 | 89 | #changelist-filter { 90 | border-left: none; 91 | border-right: none; 92 | margin-left: 0; 93 | margin-right: 30px; 94 | } 95 | 96 | #changelist-filter li.selected { 97 | border-left: none; 98 | padding-left: 10px; 99 | margin-left: 0; 100 | border-right: 5px solid var(--hairline-color); 101 | padding-right: 10px; 102 | margin-right: -15px; 103 | } 104 | 105 | #changelist table tbody td:first-child, #changelist table tbody th:first-child { 106 | border-right: none; 107 | border-left: none; 108 | } 109 | 110 | /* FORMS */ 111 | 112 | .aligned label { 113 | padding: 0 0 3px 1em; 114 | float: right; 115 | } 116 | 117 | .submit-row { 118 | text-align: left 119 | } 120 | 121 | .submit-row p.deletelink-box { 122 | float: right; 123 | } 124 | 125 | .submit-row input.default { 126 | margin-left: 0; 127 | } 128 | 129 | .vDateField, .vTimeField { 130 | margin-left: 2px; 131 | } 132 | 133 | .aligned .form-row input { 134 | margin-left: 5px; 135 | } 136 | 137 | form .aligned p.help, form .aligned div.help { 138 | clear: right; 139 | } 140 | 141 | form .aligned ul { 142 | margin-right: 163px; 143 | margin-left: 0; 144 | } 145 | 146 | form ul.inline li { 147 | float: right; 148 | padding-right: 0; 149 | padding-left: 7px; 150 | } 151 | 152 | input[type=submit].default, .submit-row input.default { 153 | float: left; 154 | } 155 | 156 | fieldset .fieldBox { 157 | float: right; 158 | margin-left: 20px; 159 | margin-right: 0; 160 | } 161 | 162 | .errorlist li { 163 | background-position: 100% 12px; 164 | padding: 0; 165 | } 166 | 167 | .errornote { 168 | background-position: 100% 12px; 169 | padding: 10px 12px; 170 | } 171 | 172 | /* WIDGETS */ 173 | 174 | .calendarnav-previous { 175 | top: 0; 176 | left: auto; 177 | right: 10px; 178 | background: url(../img/calendar-icons.svg) 0 -30px no-repeat; 179 | } 180 | 181 | .calendarbox .calendarnav-previous:focus, 182 | .calendarbox .calendarnav-previous:hover { 183 | background-position: 0 -45px; 184 | } 185 | 186 | .calendarnav-next { 187 | top: 0; 188 | right: auto; 189 | left: 10px; 190 | background: url(../img/calendar-icons.svg) 0 0 no-repeat; 191 | } 192 | 193 | .calendarbox .calendarnav-next:focus, 194 | .calendarbox .calendarnav-next:hover { 195 | background-position: 0 -15px; 196 | } 197 | 198 | .calendar caption, .calendarbox h2 { 199 | text-align: center; 200 | } 201 | 202 | .selector { 203 | float: right; 204 | } 205 | 206 | .selector .selector-filter { 207 | text-align: right; 208 | } 209 | 210 | .inline-deletelink { 211 | float: left; 212 | } 213 | 214 | form .form-row p.datetime { 215 | overflow: hidden; 216 | } 217 | 218 | .related-widget-wrapper { 219 | float: right; 220 | } 221 | 222 | /* MISC */ 223 | 224 | .inline-related h2, .inline-group h2 { 225 | text-align: right 226 | } 227 | 228 | .inline-related h3 span.delete { 229 | padding-right: 20px; 230 | padding-left: inherit; 231 | left: 10px; 232 | right: inherit; 233 | float:left; 234 | } 235 | 236 | .inline-related h3 span.delete label { 237 | margin-left: inherit; 238 | margin-right: 2px; 239 | } 240 | -------------------------------------------------------------------------------- /staticfiles/admin/css/vendor/select2/LICENSE-SELECT2.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012-2017 Kevin Brown, Igor Vaynberg, and Select2 contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /staticfiles/admin/fonts/README.txt: -------------------------------------------------------------------------------- 1 | Roboto webfont source: https://www.google.com/fonts/specimen/Roboto 2 | WOFF files extracted using https://github.com/majodev/google-webfonts-helper 3 | Weights used in this project: Light (300), Regular (400), Bold (700) 4 | -------------------------------------------------------------------------------- /staticfiles/admin/fonts/Roboto-Bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefellows/python-401-api-quickstart/aa02dd40ebaf766ed53cc41ed68cb70c2afd5e63/staticfiles/admin/fonts/Roboto-Bold-webfont.woff -------------------------------------------------------------------------------- /staticfiles/admin/fonts/Roboto-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefellows/python-401-api-quickstart/aa02dd40ebaf766ed53cc41ed68cb70c2afd5e63/staticfiles/admin/fonts/Roboto-Light-webfont.woff -------------------------------------------------------------------------------- /staticfiles/admin/fonts/Roboto-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefellows/python-401-api-quickstart/aa02dd40ebaf766ed53cc41ed68cb70c2afd5e63/staticfiles/admin/fonts/Roboto-Regular-webfont.woff -------------------------------------------------------------------------------- /staticfiles/admin/img/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Code Charm Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /staticfiles/admin/img/README.txt: -------------------------------------------------------------------------------- 1 | All icons are taken from Font Awesome (http://fontawesome.io/) project. 2 | The Font Awesome font is licensed under the SIL OFL 1.1: 3 | - https://scripts.sil.org/OFL 4 | 5 | SVG icons source: https://github.com/encharm/Font-Awesome-SVG-PNG 6 | Font-Awesome-SVG-PNG is licensed under the MIT license (see file license 7 | in current folder). 8 | -------------------------------------------------------------------------------- /staticfiles/admin/img/calendar-icons.svg: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /staticfiles/admin/img/gis/move_vertex_off.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /staticfiles/admin/img/gis/move_vertex_on.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /staticfiles/admin/img/icon-addlink.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /staticfiles/admin/img/icon-alert.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /staticfiles/admin/img/icon-calendar.svg: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /staticfiles/admin/img/icon-changelink.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /staticfiles/admin/img/icon-clock.svg: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /staticfiles/admin/img/icon-deletelink.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /staticfiles/admin/img/icon-no.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /staticfiles/admin/img/icon-unknown-alt.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /staticfiles/admin/img/icon-unknown.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /staticfiles/admin/img/icon-viewlink.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /staticfiles/admin/img/icon-yes.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /staticfiles/admin/img/inline-delete.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /staticfiles/admin/img/search.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /staticfiles/admin/img/selector-icons.svg: -------------------------------------------------------------------------------- 1 | 35 | -------------------------------------------------------------------------------- /staticfiles/admin/img/sorting-icons.svg: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /staticfiles/admin/img/tooltag-add.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /staticfiles/admin/img/tooltag-arrowright.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /staticfiles/admin/js/SelectBox.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | { 3 | const SelectBox = { 4 | cache: {}, 5 | init: function(id) { 6 | const box = document.getElementById(id); 7 | SelectBox.cache[id] = []; 8 | const cache = SelectBox.cache[id]; 9 | for (const node of box.options) { 10 | cache.push({value: node.value, text: node.text, displayed: 1}); 11 | } 12 | }, 13 | redisplay: function(id) { 14 | // Repopulate HTML select box from cache 15 | const box = document.getElementById(id); 16 | const scroll_value_from_top = box.scrollTop; 17 | box.innerHTML = ''; 18 | for (const node of SelectBox.cache[id]) { 19 | if (node.displayed) { 20 | const new_option = new Option(node.text, node.value, false, false); 21 | // Shows a tooltip when hovering over the option 22 | new_option.title = node.text; 23 | box.appendChild(new_option); 24 | } 25 | } 26 | box.scrollTop = scroll_value_from_top; 27 | }, 28 | filter: function(id, text) { 29 | // Redisplay the HTML select box, displaying only the choices containing ALL 30 | // the words in text. (It's an AND search.) 31 | const tokens = text.toLowerCase().split(/\s+/); 32 | for (const node of SelectBox.cache[id]) { 33 | node.displayed = 1; 34 | const node_text = node.text.toLowerCase(); 35 | for (const token of tokens) { 36 | if (!node_text.includes(token)) { 37 | node.displayed = 0; 38 | break; // Once the first token isn't found we're done 39 | } 40 | } 41 | } 42 | SelectBox.redisplay(id); 43 | }, 44 | delete_from_cache: function(id, value) { 45 | let delete_index = null; 46 | const cache = SelectBox.cache[id]; 47 | for (const [i, node] of cache.entries()) { 48 | if (node.value === value) { 49 | delete_index = i; 50 | break; 51 | } 52 | } 53 | cache.splice(delete_index, 1); 54 | }, 55 | add_to_cache: function(id, option) { 56 | SelectBox.cache[id].push({value: option.value, text: option.text, displayed: 1}); 57 | }, 58 | cache_contains: function(id, value) { 59 | // Check if an item is contained in the cache 60 | for (const node of SelectBox.cache[id]) { 61 | if (node.value === value) { 62 | return true; 63 | } 64 | } 65 | return false; 66 | }, 67 | move: function(from, to) { 68 | const from_box = document.getElementById(from); 69 | for (const option of from_box.options) { 70 | const option_value = option.value; 71 | if (option.selected && SelectBox.cache_contains(from, option_value)) { 72 | SelectBox.add_to_cache(to, {value: option_value, text: option.text, displayed: 1}); 73 | SelectBox.delete_from_cache(from, option_value); 74 | } 75 | } 76 | SelectBox.redisplay(from); 77 | SelectBox.redisplay(to); 78 | }, 79 | move_all: function(from, to) { 80 | const from_box = document.getElementById(from); 81 | for (const option of from_box.options) { 82 | const option_value = option.value; 83 | if (SelectBox.cache_contains(from, option_value)) { 84 | SelectBox.add_to_cache(to, {value: option_value, text: option.text, displayed: 1}); 85 | SelectBox.delete_from_cache(from, option_value); 86 | } 87 | } 88 | SelectBox.redisplay(from); 89 | SelectBox.redisplay(to); 90 | }, 91 | sort: function(id) { 92 | SelectBox.cache[id].sort(function(a, b) { 93 | a = a.text.toLowerCase(); 94 | b = b.text.toLowerCase(); 95 | if (a > b) { 96 | return 1; 97 | } 98 | if (a < b) { 99 | return -1; 100 | } 101 | return 0; 102 | } ); 103 | }, 104 | select_all: function(id) { 105 | const box = document.getElementById(id); 106 | for (const option of box.options) { 107 | option.selected = true; 108 | } 109 | } 110 | }; 111 | window.SelectBox = SelectBox; 112 | } 113 | -------------------------------------------------------------------------------- /staticfiles/admin/js/autocomplete.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | { 3 | const $ = django.jQuery; 4 | 5 | $.fn.djangoAdminSelect2 = function() { 6 | $.each(this, function(i, element) { 7 | $(element).select2({ 8 | ajax: { 9 | data: (params) => { 10 | return { 11 | term: params.term, 12 | page: params.page, 13 | app_label: element.dataset.appLabel, 14 | model_name: element.dataset.modelName, 15 | field_name: element.dataset.fieldName 16 | }; 17 | } 18 | } 19 | }); 20 | }); 21 | return this; 22 | }; 23 | 24 | $(function() { 25 | // Initialize all autocomplete widgets except the one in the template 26 | // form used when a new formset is added. 27 | $('.admin-autocomplete').not('[name*=__prefix__]').djangoAdminSelect2(); 28 | }); 29 | 30 | document.addEventListener('formset:added', (event) => { 31 | $(event.target).find('.admin-autocomplete').djangoAdminSelect2(); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /staticfiles/admin/js/cancel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | { 3 | // Call function fn when the DOM is loaded and ready. If it is already 4 | // loaded, call the function now. 5 | // http://youmightnotneedjquery.com/#ready 6 | function ready(fn) { 7 | if (document.readyState !== 'loading') { 8 | fn(); 9 | } else { 10 | document.addEventListener('DOMContentLoaded', fn); 11 | } 12 | } 13 | 14 | ready(function() { 15 | function handleClick(event) { 16 | event.preventDefault(); 17 | const params = new URLSearchParams(window.location.search); 18 | if (params.has('_popup')) { 19 | window.close(); // Close the popup. 20 | } else { 21 | window.history.back(); // Otherwise, go back. 22 | } 23 | } 24 | 25 | document.querySelectorAll('.cancel-link').forEach(function(el) { 26 | el.addEventListener('click', handleClick); 27 | }); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /staticfiles/admin/js/change_form.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | { 3 | const inputTags = ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA']; 4 | const modelName = document.getElementById('django-admin-form-add-constants').dataset.modelName; 5 | if (modelName) { 6 | const form = document.getElementById(modelName + '_form'); 7 | for (const element of form.elements) { 8 | // HTMLElement.offsetParent returns null when the element is not 9 | // rendered. 10 | if (inputTags.includes(element.tagName) && !element.disabled && element.offsetParent) { 11 | element.focus(); 12 | break; 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /staticfiles/admin/js/collapse.js: -------------------------------------------------------------------------------- 1 | /*global gettext*/ 2 | 'use strict'; 3 | { 4 | window.addEventListener('load', function() { 5 | // Add anchor tag for Show/Hide link 6 | const fieldsets = document.querySelectorAll('fieldset.collapse'); 7 | for (const [i, elem] of fieldsets.entries()) { 8 | // Don't hide if fields in this fieldset have errors 9 | if (elem.querySelectorAll('div.errors, ul.errorlist').length === 0) { 10 | elem.classList.add('collapsed'); 11 | const h2 = elem.querySelector('h2'); 12 | const link = document.createElement('a'); 13 | link.id = 'fieldsetcollapser' + i; 14 | link.className = 'collapse-toggle'; 15 | link.href = '#'; 16 | link.textContent = gettext('Show'); 17 | h2.appendChild(document.createTextNode(' (')); 18 | h2.appendChild(link); 19 | h2.appendChild(document.createTextNode(')')); 20 | } 21 | } 22 | // Add toggle to hide/show anchor tag 23 | const toggleFunc = function(ev) { 24 | if (ev.target.matches('.collapse-toggle')) { 25 | ev.preventDefault(); 26 | ev.stopPropagation(); 27 | const fieldset = ev.target.closest('fieldset'); 28 | if (fieldset.classList.contains('collapsed')) { 29 | // Show 30 | ev.target.textContent = gettext('Hide'); 31 | fieldset.classList.remove('collapsed'); 32 | } else { 33 | // Hide 34 | ev.target.textContent = gettext('Show'); 35 | fieldset.classList.add('collapsed'); 36 | } 37 | } 38 | }; 39 | document.querySelectorAll('fieldset.module').forEach(function(el) { 40 | el.addEventListener('click', toggleFunc); 41 | }); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /staticfiles/admin/js/core.js: -------------------------------------------------------------------------------- 1 | // Core JavaScript helper functions 2 | 'use strict'; 3 | 4 | // quickElement(tagType, parentReference [, textInChildNode, attribute, attributeValue ...]); 5 | function quickElement() { 6 | const obj = document.createElement(arguments[0]); 7 | if (arguments[2]) { 8 | const textNode = document.createTextNode(arguments[2]); 9 | obj.appendChild(textNode); 10 | } 11 | const len = arguments.length; 12 | for (let i = 3; i < len; i += 2) { 13 | obj.setAttribute(arguments[i], arguments[i + 1]); 14 | } 15 | arguments[1].appendChild(obj); 16 | return obj; 17 | } 18 | 19 | // "a" is reference to an object 20 | function removeChildren(a) { 21 | while (a.hasChildNodes()) { 22 | a.removeChild(a.lastChild); 23 | } 24 | } 25 | 26 | // ---------------------------------------------------------------------------- 27 | // Find-position functions by PPK 28 | // See https://www.quirksmode.org/js/findpos.html 29 | // ---------------------------------------------------------------------------- 30 | function findPosX(obj) { 31 | let curleft = 0; 32 | if (obj.offsetParent) { 33 | while (obj.offsetParent) { 34 | curleft += obj.offsetLeft - obj.scrollLeft; 35 | obj = obj.offsetParent; 36 | } 37 | } else if (obj.x) { 38 | curleft += obj.x; 39 | } 40 | return curleft; 41 | } 42 | 43 | function findPosY(obj) { 44 | let curtop = 0; 45 | if (obj.offsetParent) { 46 | while (obj.offsetParent) { 47 | curtop += obj.offsetTop - obj.scrollTop; 48 | obj = obj.offsetParent; 49 | } 50 | } else if (obj.y) { 51 | curtop += obj.y; 52 | } 53 | return curtop; 54 | } 55 | 56 | //----------------------------------------------------------------------------- 57 | // Date object extensions 58 | // ---------------------------------------------------------------------------- 59 | { 60 | Date.prototype.getTwelveHours = function() { 61 | return this.getHours() % 12 || 12; 62 | }; 63 | 64 | Date.prototype.getTwoDigitMonth = function() { 65 | return (this.getMonth() < 9) ? '0' + (this.getMonth() + 1) : (this.getMonth() + 1); 66 | }; 67 | 68 | Date.prototype.getTwoDigitDate = function() { 69 | return (this.getDate() < 10) ? '0' + this.getDate() : this.getDate(); 70 | }; 71 | 72 | Date.prototype.getTwoDigitTwelveHour = function() { 73 | return (this.getTwelveHours() < 10) ? '0' + this.getTwelveHours() : this.getTwelveHours(); 74 | }; 75 | 76 | Date.prototype.getTwoDigitHour = function() { 77 | return (this.getHours() < 10) ? '0' + this.getHours() : this.getHours(); 78 | }; 79 | 80 | Date.prototype.getTwoDigitMinute = function() { 81 | return (this.getMinutes() < 10) ? '0' + this.getMinutes() : this.getMinutes(); 82 | }; 83 | 84 | Date.prototype.getTwoDigitSecond = function() { 85 | return (this.getSeconds() < 10) ? '0' + this.getSeconds() : this.getSeconds(); 86 | }; 87 | 88 | Date.prototype.getAbbrevMonthName = function() { 89 | return typeof window.CalendarNamespace === "undefined" 90 | ? this.getTwoDigitMonth() 91 | : window.CalendarNamespace.monthsOfYearAbbrev[this.getMonth()]; 92 | }; 93 | 94 | Date.prototype.getFullMonthName = function() { 95 | return typeof window.CalendarNamespace === "undefined" 96 | ? this.getTwoDigitMonth() 97 | : window.CalendarNamespace.monthsOfYear[this.getMonth()]; 98 | }; 99 | 100 | Date.prototype.strftime = function(format) { 101 | const fields = { 102 | b: this.getAbbrevMonthName(), 103 | B: this.getFullMonthName(), 104 | c: this.toString(), 105 | d: this.getTwoDigitDate(), 106 | H: this.getTwoDigitHour(), 107 | I: this.getTwoDigitTwelveHour(), 108 | m: this.getTwoDigitMonth(), 109 | M: this.getTwoDigitMinute(), 110 | p: (this.getHours() >= 12) ? 'PM' : 'AM', 111 | S: this.getTwoDigitSecond(), 112 | w: '0' + this.getDay(), 113 | x: this.toLocaleDateString(), 114 | X: this.toLocaleTimeString(), 115 | y: ('' + this.getFullYear()).substr(2, 4), 116 | Y: '' + this.getFullYear(), 117 | '%': '%' 118 | }; 119 | let result = '', i = 0; 120 | while (i < format.length) { 121 | if (format.charAt(i) === '%') { 122 | result = result + fields[format.charAt(i + 1)]; 123 | ++i; 124 | } 125 | else { 126 | result = result + format.charAt(i); 127 | } 128 | ++i; 129 | } 130 | return result; 131 | }; 132 | 133 | // ---------------------------------------------------------------------------- 134 | // String object extensions 135 | // ---------------------------------------------------------------------------- 136 | String.prototype.strptime = function(format) { 137 | const split_format = format.split(/[.\-/]/); 138 | const date = this.split(/[.\-/]/); 139 | let i = 0; 140 | let day, month, year; 141 | while (i < split_format.length) { 142 | switch (split_format[i]) { 143 | case "%d": 144 | day = date[i]; 145 | break; 146 | case "%m": 147 | month = date[i] - 1; 148 | break; 149 | case "%Y": 150 | year = date[i]; 151 | break; 152 | case "%y": 153 | // A %y value in the range of [00, 68] is in the current 154 | // century, while [69, 99] is in the previous century, 155 | // according to the Open Group Specification. 156 | if (parseInt(date[i], 10) >= 69) { 157 | year = date[i]; 158 | } else { 159 | year = (new Date(Date.UTC(date[i], 0))).getUTCFullYear() + 100; 160 | } 161 | break; 162 | } 163 | ++i; 164 | } 165 | // Create Date object from UTC since the parsed value is supposed to be 166 | // in UTC, not local time. Also, the calendar uses UTC functions for 167 | // date extraction. 168 | return new Date(Date.UTC(year, month, day)); 169 | }; 170 | } 171 | -------------------------------------------------------------------------------- /staticfiles/admin/js/filters.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Persist changelist filters state (collapsed/expanded). 3 | */ 4 | 'use strict'; 5 | { 6 | // Init filters. 7 | let filters = JSON.parse(sessionStorage.getItem('django.admin.filtersState')); 8 | 9 | if (!filters) { 10 | filters = {}; 11 | } 12 | 13 | Object.entries(filters).forEach(([key, value]) => { 14 | const detailElement = document.querySelector(`[data-filter-title='${key}']`); 15 | 16 | // Check if the filter is present, it could be from other view. 17 | if (detailElement) { 18 | value ? detailElement.setAttribute('open', '') : detailElement.removeAttribute('open'); 19 | } 20 | }); 21 | 22 | // Save filter state when clicks. 23 | const details = document.querySelectorAll('details'); 24 | details.forEach(detail => { 25 | detail.addEventListener('toggle', event => { 26 | filters[`${event.target.dataset.filterTitle}`] = detail.open; 27 | sessionStorage.setItem('django.admin.filtersState', JSON.stringify(filters)); 28 | }); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /staticfiles/admin/js/jquery.init.js: -------------------------------------------------------------------------------- 1 | /*global jQuery:false*/ 2 | 'use strict'; 3 | /* Puts the included jQuery into our own namespace using noConflict and passing 4 | * it 'true'. This ensures that the included jQuery doesn't pollute the global 5 | * namespace (i.e. this preserves pre-existing values for both window.$ and 6 | * window.jQuery). 7 | */ 8 | window.django = {jQuery: jQuery.noConflict(true)}; 9 | -------------------------------------------------------------------------------- /staticfiles/admin/js/nav_sidebar.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | { 3 | const toggleNavSidebar = document.getElementById('toggle-nav-sidebar'); 4 | if (toggleNavSidebar !== null) { 5 | const navLinks = document.querySelectorAll('#nav-sidebar a'); 6 | function disableNavLinkTabbing() { 7 | for (const navLink of navLinks) { 8 | navLink.tabIndex = -1; 9 | } 10 | } 11 | function enableNavLinkTabbing() { 12 | for (const navLink of navLinks) { 13 | navLink.tabIndex = 0; 14 | } 15 | } 16 | function disableNavFilterTabbing() { 17 | document.getElementById('nav-filter').tabIndex = -1; 18 | } 19 | function enableNavFilterTabbing() { 20 | document.getElementById('nav-filter').tabIndex = 0; 21 | } 22 | 23 | const main = document.getElementById('main'); 24 | let navSidebarIsOpen = localStorage.getItem('django.admin.navSidebarIsOpen'); 25 | if (navSidebarIsOpen === null) { 26 | navSidebarIsOpen = 'true'; 27 | } 28 | if (navSidebarIsOpen === 'false') { 29 | disableNavLinkTabbing(); 30 | disableNavFilterTabbing(); 31 | } 32 | main.classList.toggle('shifted', navSidebarIsOpen === 'true'); 33 | 34 | toggleNavSidebar.addEventListener('click', function() { 35 | if (navSidebarIsOpen === 'true') { 36 | navSidebarIsOpen = 'false'; 37 | disableNavLinkTabbing(); 38 | disableNavFilterTabbing(); 39 | } else { 40 | navSidebarIsOpen = 'true'; 41 | enableNavLinkTabbing(); 42 | enableNavFilterTabbing(); 43 | } 44 | localStorage.setItem('django.admin.navSidebarIsOpen', navSidebarIsOpen); 45 | main.classList.toggle('shifted'); 46 | }); 47 | } 48 | 49 | function initSidebarQuickFilter() { 50 | const options = []; 51 | const navSidebar = document.getElementById('nav-sidebar'); 52 | if (!navSidebar) { 53 | return; 54 | } 55 | navSidebar.querySelectorAll('th[scope=row] a').forEach((container) => { 56 | options.push({title: container.innerHTML, node: container}); 57 | }); 58 | 59 | function checkValue(event) { 60 | let filterValue = event.target.value; 61 | if (filterValue) { 62 | filterValue = filterValue.toLowerCase(); 63 | } 64 | if (event.key === 'Escape') { 65 | filterValue = ''; 66 | event.target.value = ''; // clear input 67 | } 68 | let matches = false; 69 | for (const o of options) { 70 | let displayValue = ''; 71 | if (filterValue) { 72 | if (o.title.toLowerCase().indexOf(filterValue) === -1) { 73 | displayValue = 'none'; 74 | } else { 75 | matches = true; 76 | } 77 | } 78 | // show/hide parent
to respect formatting */
19 | }
20 |
21 | .main-container {
22 | padding-left: 30px;
23 | padding-right: 30px;
24 | }
25 |
26 | .btn:focus,
27 | .btn:focus:active {
28 | outline: none;
29 | }
30 |
31 | .sidebar {
32 | overflow: auto;
33 | font-family: verdana, sans-serif;
34 | font-size: 12px;
35 | font-weight: 200;
36 | background-color: #2e353d;
37 | position: fixed;
38 | top: 0px;
39 | width: 225px;
40 | height: 100%;
41 | color: #FFF;
42 | }
43 |
44 | .sidebar .brand {
45 | background-color: #23282e;
46 | display: block;
47 | text-align: center;
48 | padding: 25px 0;
49 | margin-top: 0;
50 | margin-bottom: 0;
51 | }
52 |
53 | .sidebar .brand a {
54 | color: #FFF;
55 | }
56 |
57 | .sidebar .brand a:hover,
58 | .sidebar .brand a:active,
59 | .sidebar .brand a:focus {
60 | text-decoration: none;
61 | }
62 |
63 | .sidebar .toggle-btn {
64 | display: none;
65 | }
66 |
67 | .sidebar .menu-list {
68 | width: inherit;
69 | }
70 |
71 | .sidebar .menu-list ul,
72 | .sidebar .menu-list li {
73 | background: #2e353d;
74 | list-style: none;
75 | padding: 0px;
76 | margin: 0px;
77 | line-height: 35px;
78 | cursor: pointer;
79 | }
80 |
81 | .sidebar .menu-list ul :not(collapsed) .arrow:before,
82 | .sidebar .menu-list li :not(collapsed) .arrow:before {
83 | font-family: FontAwesome;
84 | content: "\f078";
85 | display: inline-block;
86 | padding-left: 10px;
87 | padding-right: 10px;
88 | vertical-align: middle;
89 | float: right;
90 | }
91 |
92 | .sidebar .menu-list ul .active,
93 | .sidebar .menu-list li .active {
94 | border-left: 3px solid #d19b3d;
95 | background-color: #4f5b69;
96 | }
97 |
98 | .sidebar .menu-list ul .sub-menu li.active,
99 | .sidebar .menu-list li .sub-menu li.active {
100 | color: #d19b3d;
101 | }
102 |
103 | .sidebar .menu-list ul .sub-menu li.active a,
104 | .sidebar .menu-list li .sub-menu li.active a {
105 | color: #d19b3d;
106 | }
107 |
108 | .sidebar .menu-list ul .sub-menu li,
109 | .sidebar .menu-list li .sub-menu li {
110 | background-color: #181c20;
111 | border: none;
112 | border-bottom: 1px solid #23282e;
113 | margin-left: 0px;
114 | line-height: 1.4;
115 | padding-top: 10px;
116 | padding-bottom: 10px;
117 | padding-right: 10px;
118 | padding-left: 25px;
119 | }
120 |
121 | .sidebar .menu-list ul .sub-menu li:hover,
122 | .sidebar .menu-list li .sub-menu li:hover {
123 | background-color: #020203;
124 | }
125 |
126 |
127 | .sidebar .menu-list ul .sub-menu li a,
128 | .sidebar .menu-list li .sub-menu li a {
129 | display: block;
130 | }
131 |
132 | .sidebar .menu-list ul .sub-menu li a:before,
133 | .sidebar .menu-list li .sub-menu li a:before {
134 | font-family: FontAwesome;
135 | font-size: 14px;
136 | font-weight: bold;
137 | content: "\f105";
138 | display: inline;
139 | vertical-align: middle;
140 | padding-left: 0;
141 | padding-right: 7px;
142 | margin-left: -12px;
143 | }
144 |
145 | .sidebar .menu-list li {
146 | padding-left: 0px;
147 | border-left: 3px solid #2e353d;
148 | border-bottom: 1px solid #23282e;
149 | }
150 |
151 | .sidebar .menu-list li a {
152 | text-decoration: none;
153 | color: white;
154 | }
155 |
156 | .sidebar .menu-list li a i {
157 | padding-left: 10px;
158 | width: 20px;
159 | padding-right: 20px;
160 | }
161 |
162 | .sidebar .menu-list li:hover {
163 | border-left: 3px solid #d19b3d;
164 | background-color: #4f5b69;
165 | -webkit-transition: all 1s ease;
166 | -moz-transition: all 1s ease;
167 | -o-transition: all 1s ease;
168 | -ms-transition: all 1s ease;
169 | transition: all 1s ease;
170 | }
171 |
172 | .sidebar #menu-content {
173 | padding-bottom: 70px;
174 | }
175 |
176 | body {
177 | margin: 0px;
178 | padding: 0px;
179 | }
180 |
181 | .coredocs-section-title {
182 | margin-top: 20px;
183 | padding-bottom: 10px;
184 | border-bottom: 1px solid lightgrey;
185 | }
186 |
187 | .coredocs-link-title a,
188 | .coredocs-section-title a {
189 | display: none;
190 | }
191 |
192 | .coredocs-link-title a,
193 | .coredocs-section-title a {
194 | text-decoration: none;
195 | }
196 |
197 | .coredocs-link-title:hover a,
198 | .coredocs-section-title:hover a {
199 | display: inline;
200 | font-size: 20px;
201 | }
202 |
203 | .coredocs-section-title:last-child {
204 | margin-top: 0;
205 | }
206 |
207 |
208 | /* @group Language Switcher */
209 |
210 | .sidebar .menu-list.menu-list-bottom {
211 | margin-bottom: 0;
212 | position: fixed;
213 | width: inherit;
214 | bottom: 0;
215 | left: 0;
216 | right: 0;
217 | border-top: 1px solid #23282e;
218 | }
219 |
220 | .sidebar .menu-list-bottom li span {
221 | float: right;
222 | margin-right: 20px;
223 | color: #d19b3d;
224 | }
225 |
226 | /* @end Language Switcher */
227 |
228 |
229 | /* @group Docs Content */
230 |
231 | .docs-content .meta .label {
232 | vertical-align: middle;
233 | font-size: 14px;
234 | font-weight: normal;
235 | }
236 |
237 | .docs-content .meta code {
238 | vertical-align: middle;
239 | padding: .2em .6em .3em;
240 | font-size: 14px;
241 | }
242 |
243 | .docs-content .btn {
244 | font-size: inherit;
245 | }
246 |
247 | .code-samples pre {
248 | margin-top: 20px;
249 | }
250 |
251 | /* @end Docs Content */
252 |
253 |
254 | @media (max-width: 767px) {
255 | .main-container {
256 | padding-left: 15px;
257 | padding-right: 15px;
258 | }
259 |
260 | .sidebar {
261 | position: relative;
262 | width: 100%;
263 | margin-bottom: 10px;
264 | overflow: visible;
265 | }
266 |
267 | .sidebar .toggle-btn {
268 | display: block;
269 | cursor: pointer;
270 | position: absolute;
271 | right: 10px;
272 | top: 10px;
273 | z-index: 10 !important;
274 | padding: 3px;
275 | width: 40px;
276 | text-align: center;
277 | }
278 |
279 | .sidebar .menu-list.menu-list-bottom {
280 | position: static;
281 | }
282 |
283 | .sidebar .brand {
284 | margin-top: 0;
285 | margin-bottom: 0;
286 |
287 | text-align: left !important;
288 | font-size: 22px;
289 | padding: 0;
290 | padding-left: 20px;
291 | line-height: 50px !important;
292 | }
293 | }
294 |
295 | @media (min-width: 767px) {
296 | .sidebar .menu-list .menu-content {
297 | display: block;
298 | }
299 | #main {
300 | width:calc(100% - 225px);
301 | float: right;
302 | }
303 | }
304 |
305 | @media (min-width: 992px) {
306 | .modal-lg {
307 | width: 980px;
308 | }
309 | }
310 |
311 | .api-modal .modal-title .fa {
312 | color: #93c54b;
313 | }
314 |
315 | .api-modal .modal-body .request-awaiting {
316 | padding: 35px 10px;
317 | color: #7F8177;
318 | text-align: center;
319 | }
320 |
321 | .api-modal .modal-body .meta {
322 | margin-bottom: 20px;
323 | }
324 |
325 | .api-modal .modal-body .meta .label {
326 | vertical-align: middle;
327 | font-size: 14px;
328 | font-weight: normal;
329 | }
330 |
331 | .api-modal .modal-body .meta code {
332 | vertical-align: middle;
333 | padding: .2em .6em .3em;
334 | font-size: 14px;
335 | }
336 |
337 | .api-modal .modal-content .toggle-view {
338 | text-align: right;
339 | float: right;
340 | }
341 |
342 | .api-modal .modal-content .response .well {
343 | margin: 0;
344 | max-height: 550px;
345 | }
346 |
347 | .highlight {
348 | background-color: #f7f7f9
349 | }
350 |
351 | .checkbox label.control-label {
352 | font-weight: bold
353 | }
354 |
355 | @media (min-width: 768px) {
356 | .navbar-nav.navbar-right:last-child {
357 | margin-right: 0 !important;
358 | }
359 | }
360 |
--------------------------------------------------------------------------------
/staticfiles/rest_framework/docs/css/highlight.css:
--------------------------------------------------------------------------------
1 | /*
2 | This is the GitHub theme for highlight.js
3 |
4 | github.com style (c) Vasily Polovnyov
5 |
6 | */
7 |
8 | .hljs {
9 | display: block;
10 | overflow-x: auto;
11 | padding: 0.5em;
12 | color: #333;
13 | -webkit-text-size-adjust: none;
14 | }
15 |
16 | .hljs-comment,
17 | .diff .hljs-header,
18 | .hljs-javadoc {
19 | color: #998;
20 | font-style: italic;
21 | }
22 |
23 | .hljs-keyword,
24 | .css .rule .hljs-keyword,
25 | .hljs-winutils,
26 | .nginx .hljs-title,
27 | .hljs-subst,
28 | .hljs-request,
29 | .hljs-status {
30 | color: #333;
31 | font-weight: bold;
32 | }
33 |
34 | .hljs-number,
35 | .hljs-hexcolor,
36 | .ruby .hljs-constant {
37 | color: #008080;
38 | }
39 |
40 | .hljs-string,
41 | .hljs-tag .hljs-value,
42 | .hljs-phpdoc,
43 | .hljs-dartdoc,
44 | .tex .hljs-formula {
45 | color: #d14;
46 | }
47 |
48 | .hljs-title,
49 | .hljs-id,
50 | .scss .hljs-preprocessor {
51 | color: #900;
52 | font-weight: bold;
53 | }
54 |
55 | .hljs-list .hljs-keyword,
56 | .hljs-subst {
57 | font-weight: normal;
58 | }
59 |
60 | .hljs-class .hljs-title,
61 | .hljs-type,
62 | .vhdl .hljs-literal,
63 | .tex .hljs-command {
64 | color: #458;
65 | font-weight: bold;
66 | }
67 |
68 | .hljs-tag,
69 | .hljs-tag .hljs-title,
70 | .hljs-rule .hljs-property,
71 | .django .hljs-tag .hljs-keyword {
72 | color: #000080;
73 | font-weight: normal;
74 | }
75 |
76 | .hljs-attribute,
77 | .hljs-variable,
78 | .lisp .hljs-body,
79 | .hljs-name {
80 | color: #008080;
81 | }
82 |
83 | .hljs-regexp {
84 | color: #009926;
85 | }
86 |
87 | .hljs-symbol,
88 | .ruby .hljs-symbol .hljs-string,
89 | .lisp .hljs-keyword,
90 | .clojure .hljs-keyword,
91 | .scheme .hljs-keyword,
92 | .tex .hljs-special,
93 | .hljs-prompt {
94 | color: #990073;
95 | }
96 |
97 | .hljs-built_in {
98 | color: #0086b3;
99 | }
100 |
101 | .hljs-preprocessor,
102 | .hljs-pragma,
103 | .hljs-pi,
104 | .hljs-doctype,
105 | .hljs-shebang,
106 | .hljs-cdata {
107 | color: #999;
108 | font-weight: bold;
109 | }
110 |
111 | .hljs-deletion {
112 | background: #fdd;
113 | }
114 |
115 | .hljs-addition {
116 | background: #dfd;
117 | }
118 |
119 | .diff .hljs-change {
120 | background: #0086b3;
121 | }
122 |
123 | .hljs-chunk {
124 | color: #aaa;
125 | }
126 |
--------------------------------------------------------------------------------
/staticfiles/rest_framework/docs/css/jquery.json-view.min.css:
--------------------------------------------------------------------------------
1 | .json-view{position:relative}
2 | .json-view .collapser{width:20px;height:18px;display:block;position:absolute;left:-1.7em;top:-.2em;z-index:5;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAD1JREFUeNpiYGBgOADE%2F3Hgw0DM4IRHgSsDFOzFInmMAQnY49ONzZRjDFiADT7dMLALiE8y4AGW6LoBAgwAuIkf%2F%2FB7O9sAAAAASUVORK5CYII%3D);background-repeat:no-repeat;background-position:center center;opacity:.5;cursor:pointer}
3 | .json-view .collapsed{-ms-transform:rotate(-90deg);-moz-transform:rotate(-90deg);-khtml-transform:rotate(-90deg);-webkit-transform:rotate(-90deg);-o-transform:rotate(-90deg);transform:rotate(-90deg)}
4 | .json-view .bl{display:block;padding-left:20px;margin-left:-20px;position:relative}
5 | .json-view{font-family:monospace}
6 | .json-view ul{list-style-type:none;padding-left:2em;border-left:1px dotted;margin:.3em}
7 | .json-view ul li{position:relative}
8 | .json-view .comments,.json-view .dots{display:none;-moz-user-select:none;-ms-user-select:none;-khtml-user-select:none;-webkit-user-select:none;-o-user-select:none;user-select:none}
9 | .json-view .comments{padding-left:.8em;font-style:italic;color:#888}
10 | .json-view .bool,.json-view .null,.json-view .num,.json-view .undef{font-weight:700;color:#1A01CC}
11 | .json-view .str{color:#800}
--------------------------------------------------------------------------------
/staticfiles/rest_framework/docs/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefellows/python-401-api-quickstart/aa02dd40ebaf766ed53cc41ed68cb70c2afd5e63/staticfiles/rest_framework/docs/img/favicon.ico
--------------------------------------------------------------------------------
/staticfiles/rest_framework/docs/img/grid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefellows/python-401-api-quickstart/aa02dd40ebaf766ed53cc41ed68cb70c2afd5e63/staticfiles/rest_framework/docs/img/grid.png
--------------------------------------------------------------------------------
/staticfiles/rest_framework/docs/js/jquery.json-view.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * jquery.json-view - jQuery collapsible JSON plugin
3 | * @version v1.0.0
4 | * @link http://github.com/bazh/jquery.json-view
5 | * @license MIT
6 | */
7 | !function(e){"use strict";var n=function(n){var a=e("",{"class":"collapser",on:{click:function(){var n=e(this);n.toggleClass("collapsed");var a=n.parent().children(".block"),p=a.children("ul");n.hasClass("collapsed")?(p.hide(),a.children(".dots, .comments").show()):(p.show(),a.children(".dots, .comments").hide())}}});return n&&a.addClass("collapsed"),a},a=function(a,p){var t=e.extend({},{nl2br:!0},p),r=function(e){return e.toString()?e.toString().replace(/&/g,"&").replace(/"/g,""").replace(//g,">"):""},s=function(n,a){return e("",{"class":a,html:r(n)})},l=function(a,p){switch(e.type(a)){case"object":p||(p=0);var c=e("",{"class":"block"}),d=Object.keys(a).length;if(!d)return c.append(s("{","b")).append(" ").append(s("}","b"));c.append(s("{","b"));var i=e("
",{"class":"obj collapsible level"+p});return e.each(a,function(a,t){d--;var r=e("").append(s('"',"q")).append(a).append(s('"',"q")).append(": ").append(l(t,p+1));-1===["object","array"].indexOf(e.type(t))||e.isEmptyObject(t)||r.prepend(n()),d>0&&r.append(","),i.append(r)}),c.append(i),c.append(s("...","dots")),c.append(s("}","b")),c.append(1===Object.keys(a).length?s("// 1 item","comments"):s("// "+Object.keys(a).length+" items","comments")),c;case"array":p||(p=0);var d=a.length,c=e("",{"class":"block"});if(!d)return c.append(s("[","b")).append(" ").append(s("]","b"));c.append(s("[","b"));var i=e("
",{"class":"obj collapsible level"+p});return e.each(a,function(a,t){d--;var r=e("").append(l(t,p+1));-1===["object","array"].indexOf(e.type(t))||e.isEmptyObject(t)||r.prepend(n()),d>0&&r.append(","),i.append(r)}),c.append(i),c.append(s("...","dots")),c.append(s("]","b")),c.append(1===a.length?s("// 1 item","comments"):s("// "+a.length+" items","comments")),c;case"string":if(a=r(a),/^(http|https|file):\/\/[^\s]+$/i.test(a))return e("").append(s('"',"q")).append(e("",{href:a,text:a})).append(s('"',"q"));if(t.nl2br){var o=/\n/g;o.test(a)&&(a=(a+"").replace(o,"
"))}var u=e("",{"class":"str"}).html(a);return e("").append(s('"',"q")).append(u).append(s('"',"q"));case"number":return s(a.toString(),"num");case"undefined":return s("undefined","undef");case"null":return s("null","null");case"boolean":return s(a?"true":"false","bool")}};return l(a)};return e.fn.jsonView=function(n,p){var t=e(this);if(p=e.extend({},{nl2br:!0},p),"string"==typeof n)try{n=JSON.parse(n)}catch(r){}return t.append(e("",{"class":"json-view"}).append(a(n,p))),t}}(jQuery);
--------------------------------------------------------------------------------
/staticfiles/rest_framework/fonts/fontawesome-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefellows/python-401-api-quickstart/aa02dd40ebaf766ed53cc41ed68cb70c2afd5e63/staticfiles/rest_framework/fonts/fontawesome-webfont.eot
--------------------------------------------------------------------------------
/staticfiles/rest_framework/fonts/fontawesome-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefellows/python-401-api-quickstart/aa02dd40ebaf766ed53cc41ed68cb70c2afd5e63/staticfiles/rest_framework/fonts/fontawesome-webfont.ttf
--------------------------------------------------------------------------------
/staticfiles/rest_framework/fonts/fontawesome-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefellows/python-401-api-quickstart/aa02dd40ebaf766ed53cc41ed68cb70c2afd5e63/staticfiles/rest_framework/fonts/fontawesome-webfont.woff
--------------------------------------------------------------------------------
/staticfiles/rest_framework/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefellows/python-401-api-quickstart/aa02dd40ebaf766ed53cc41ed68cb70c2afd5e63/staticfiles/rest_framework/fonts/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/staticfiles/rest_framework/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefellows/python-401-api-quickstart/aa02dd40ebaf766ed53cc41ed68cb70c2afd5e63/staticfiles/rest_framework/fonts/glyphicons-halflings-regular.ttf
--------------------------------------------------------------------------------
/staticfiles/rest_framework/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefellows/python-401-api-quickstart/aa02dd40ebaf766ed53cc41ed68cb70c2afd5e63/staticfiles/rest_framework/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/staticfiles/rest_framework/fonts/glyphicons-halflings-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefellows/python-401-api-quickstart/aa02dd40ebaf766ed53cc41ed68cb70c2afd5e63/staticfiles/rest_framework/fonts/glyphicons-halflings-regular.woff2
--------------------------------------------------------------------------------
/staticfiles/rest_framework/img/glyphicons-halflings-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefellows/python-401-api-quickstart/aa02dd40ebaf766ed53cc41ed68cb70c2afd5e63/staticfiles/rest_framework/img/glyphicons-halflings-white.png
--------------------------------------------------------------------------------
/staticfiles/rest_framework/img/glyphicons-halflings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefellows/python-401-api-quickstart/aa02dd40ebaf766ed53cc41ed68cb70c2afd5e63/staticfiles/rest_framework/img/glyphicons-halflings.png
--------------------------------------------------------------------------------
/staticfiles/rest_framework/img/grid.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefellows/python-401-api-quickstart/aa02dd40ebaf766ed53cc41ed68cb70c2afd5e63/staticfiles/rest_framework/img/grid.png
--------------------------------------------------------------------------------
/staticfiles/rest_framework/js/ajax-form.js:
--------------------------------------------------------------------------------
1 | function replaceDocument(docString) {
2 | var doc = document.open("text/html");
3 |
4 | doc.write(docString);
5 | doc.close();
6 | }
7 |
8 | function doAjaxSubmit(e) {
9 | var form = $(this);
10 | var btn = $(this.clk);
11 | var method = (
12 | btn.data('method') ||
13 | form.data('method') ||
14 | form.attr('method') || 'GET'
15 | ).toUpperCase();
16 |
17 | if (method === 'GET') {
18 | // GET requests can always use standard form submits.
19 | return;
20 | }
21 |
22 | var contentType =
23 | form.find('input[data-override="content-type"]').val() ||
24 | form.find('select[data-override="content-type"] option:selected').text();
25 |
26 | if (method === 'POST' && !contentType) {
27 | // POST requests can use standard form submits, unless we have
28 | // overridden the content type.
29 | return;
30 | }
31 |
32 | // At this point we need to make an AJAX form submission.
33 | e.preventDefault();
34 |
35 | var url = form.attr('action');
36 | var data;
37 |
38 | if (contentType) {
39 | data = form.find('[data-override="content"]').val() || ''
40 |
41 | if (contentType === 'multipart/form-data') {
42 | // We need to add a boundary parameter to the header
43 | // We assume the first valid-looking boundary line in the body is correct
44 | // regex is from RFC 2046 appendix A
45 | var boundaryCharNoSpace = "0-9A-Z'()+_,-./:=?";
46 | var boundaryChar = boundaryCharNoSpace + ' ';
47 | var re = new RegExp('^--([' + boundaryChar + ']{0,69}[' + boundaryCharNoSpace + '])[\\s]*?$', 'im');
48 | var boundary = data.match(re);
49 | if (boundary !== null) {
50 | contentType += '; boundary="' + boundary[1] + '"';
51 | }
52 | // Fix textarea.value EOL normalisation (multipart/form-data should use CR+NL, not NL)
53 | data = data.replace(/\n/g, '\r\n');
54 | }
55 | } else {
56 | contentType = form.attr('enctype') || form.attr('encoding')
57 |
58 | if (contentType === 'multipart/form-data') {
59 | if (!window.FormData) {
60 | alert('Your browser does not support AJAX multipart form submissions');
61 | return;
62 | }
63 |
64 | // Use the FormData API and allow the content type to be set automatically,
65 | // so it includes the boundary string.
66 | // See https://developer.mozilla.org/en-US/docs/Web/API/FormData/Using_FormData_Objects
67 | contentType = false;
68 | data = new FormData(form[0]);
69 | } else {
70 | contentType = 'application/x-www-form-urlencoded; charset=UTF-8'
71 | data = form.serialize();
72 | }
73 | }
74 |
75 | var ret = $.ajax({
76 | url: url,
77 | method: method,
78 | data: data,
79 | contentType: contentType,
80 | processData: false,
81 | headers: {
82 | 'Accept': 'text/html; q=1.0, */*'
83 | },
84 | });
85 |
86 | ret.always(function(data, textStatus, jqXHR) {
87 | if (textStatus != 'success') {
88 | jqXHR = data;
89 | }
90 |
91 | var responseContentType = jqXHR.getResponseHeader("content-type") || "";
92 |
93 | if (responseContentType.toLowerCase().indexOf('text/html') === 0) {
94 | replaceDocument(jqXHR.responseText);
95 |
96 | try {
97 | // Modify the location and scroll to top, as if after page load.
98 | history.replaceState({}, '', url);
99 | scroll(0, 0);
100 | } catch (err) {
101 | // History API not supported, so redirect.
102 | window.location = url;
103 | }
104 | } else {
105 | // Not HTML content. We can't open this directly, so redirect.
106 | window.location = url;
107 | }
108 | });
109 |
110 | return ret;
111 | }
112 |
113 | function captureSubmittingElement(e) {
114 | var target = e.target;
115 | var form = this;
116 |
117 | form.clk = target;
118 | }
119 |
120 | $.fn.ajaxForm = function() {
121 | var options = {}
122 |
123 | return this
124 | .unbind('submit.form-plugin click.form-plugin')
125 | .bind('submit.form-plugin', options, doAjaxSubmit)
126 | .bind('click.form-plugin', options, captureSubmittingElement);
127 | };
128 |
--------------------------------------------------------------------------------
/staticfiles/rest_framework/js/csrf.js:
--------------------------------------------------------------------------------
1 | function getCookie(name) {
2 | var cookieValue = null;
3 |
4 | if (document.cookie && document.cookie != '') {
5 | var cookies = document.cookie.split(';');
6 |
7 | for (var i = 0; i < cookies.length; i++) {
8 | var cookie = jQuery.trim(cookies[i]);
9 |
10 | // Does this cookie string begin with the name we want?
11 | if (cookie.substring(0, name.length + 1) == (name + '=')) {
12 | cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
13 | break;
14 | }
15 | }
16 | }
17 |
18 | return cookieValue;
19 | }
20 |
21 | function csrfSafeMethod(method) {
22 | // these HTTP methods do not require CSRF protection
23 | return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
24 | }
25 |
26 | function sameOrigin(url) {
27 | // test that a given url is a same-origin URL
28 | // url could be relative or scheme relative or absolute
29 | var host = document.location.host; // host + port
30 | var protocol = document.location.protocol;
31 | var sr_origin = '//' + host;
32 | var origin = protocol + sr_origin;
33 |
34 | // Allow absolute or scheme relative URLs to same origin
35 | return (url == origin || url.slice(0, origin.length + 1) == origin + '/') ||
36 | (url == sr_origin || url.slice(0, sr_origin.length + 1) == sr_origin + '/') ||
37 | // or any other URL that isn't scheme relative or absolute i.e relative.
38 | !(/^(\/\/|http:|https:).*/.test(url));
39 | }
40 |
41 | var csrftoken = window.drf.csrfToken;
42 |
43 | $.ajaxSetup({
44 | beforeSend: function(xhr, settings) {
45 | if (!csrfSafeMethod(settings.type) && sameOrigin(settings.url)) {
46 | // Send the token to same-origin, relative URLs only.
47 | // Send the token only if the method warrants CSRF protection
48 | // Using the CSRFToken value acquired earlier
49 | xhr.setRequestHeader(window.drf.csrfHeaderName, csrftoken);
50 | }
51 | }
52 | });
53 |
--------------------------------------------------------------------------------
/staticfiles/rest_framework/js/default.js:
--------------------------------------------------------------------------------
1 | $(document).ready(function() {
2 | // JSON highlighting.
3 | prettyPrint();
4 |
5 | // Bootstrap tooltips.
6 | $('.js-tooltip').tooltip({
7 | delay: 1000,
8 | container: 'body'
9 | });
10 |
11 | // Deal with rounded tab styling after tab clicks.
12 | $('a[data-toggle="tab"]:first').on('shown', function(e) {
13 | $(e.target).parents('.tabbable').addClass('first-tab-active');
14 | });
15 |
16 | $('a[data-toggle="tab"]:not(:first)').on('shown', function(e) {
17 | $(e.target).parents('.tabbable').removeClass('first-tab-active');
18 | });
19 |
20 | $('a[data-toggle="tab"]').click(function() {
21 | document.cookie = "tabstyle=" + this.name + "; path=/";
22 | });
23 |
24 | // Store tab preference in cookies & display appropriate tab on load.
25 | var selectedTab = null;
26 | var selectedTabName = getCookie('tabstyle');
27 |
28 | if (selectedTabName) {
29 | selectedTabName = selectedTabName.replace(/[^a-z-]/g, '');
30 | }
31 |
32 | if (selectedTabName) {
33 | selectedTab = $('.form-switcher a[name=' + selectedTabName + ']');
34 | }
35 |
36 | if (selectedTab && selectedTab.length > 0) {
37 | // Display whichever tab is selected.
38 | selectedTab.tab('show');
39 | } else {
40 | // If no tab selected, display rightmost tab.
41 | $('.form-switcher a:first').tab('show');
42 | }
43 |
44 | $(window).on('load', function() {
45 | $('#errorModal').modal('show');
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/templates/about.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block content %}
4 |
5 |
6 |
7 |
11 |
12 |
26 |
25 |
35 |
36 |
37 | {% endblock content %}
38 |
--------------------------------------------------------------------------------
/templates/base.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 |
3 |
4 |
5 |
6 |
7 |
9 |
10 | {% block title %}All the Things{% endblock %}
11 |
12 |
13 |
14 |
15 |
47 |
48 |
49 |
50 | {% block content %}
51 | {% endblock content %}
52 |
53 |
54 |
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/templates/home.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load static %}
3 |
4 | {% block title %}All the Things - Home{% endblock %}
5 |
6 | {% block content %}
7 | {% if user.is_authenticated %}
8 |
9 |
10 | Hi {{ user.username }}!
11 | Maybe one day you'll see a glorious home page here.
12 | But for now just notice that List and Create are in Nav bar.
13 |
14 |
15 | {% else %}
16 |
17 |
18 |
19 | {# Thanks to https://unsplash.com/photos/Kw_zQBAChws?utm_source=unsplash&utm_medium=referral&utm_content=creditShareLink#}
20 |
21 |
22 | Things
23 | Almanac, binder, cucumber, drill, envelope, friend, grill, hay, jam, koala, lanyard, mousetrap, necklace, oil, printer, quill, restroom, saline, treasure, wings, xylophone, zebra, and more!
24 | Get Started
25 |
26 |
27 |
28 |
29 | {% endif %}
30 | {% endblock %}
31 |
--------------------------------------------------------------------------------
/templates/registration/login.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}Log In{% endblock %}
4 |
5 | {% block content %}
6 | {#Log In
#}
7 | {##}
12 |
13 |
14 |
15 |
35 |
36 |
37 | {% endblock %}
38 |
--------------------------------------------------------------------------------
/templates/registration/signup.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}Sign Up{% endblock %}
4 |
5 | {% block content %}
6 | {# Sign Up
#}
7 | {# #}
12 |
13 |
15 |
56 |
57 | {% endblock %}
58 |
--------------------------------------------------------------------------------
/templates/things/thing_create.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block content %}
4 |
5 |
6 |
8 |
40 |
41 |
42 | {% endblock content %}
43 |
--------------------------------------------------------------------------------
/templates/things/thing_delete.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block content %}
4 | Delete Thing
5 |
10 | {% endblock content %}
11 |
--------------------------------------------------------------------------------
/templates/things/thing_detail.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block content %}
4 |
5 |
6 |
7 |
31 |
32 | Reviewed by: {{
33 | thing.reviewer }}
34 |
35 |
36 | {{ thing.description }}
37 |
38 |
39 |
40 |
41 | {% for i in "12345" %}
42 |
43 |
51 | {% endfor %}
52 |
53 |
54 |
55 |
56 |
58 | Read more
59 |
65 |
66 |
67 |
68 |
69 |
70 | {% endblock content %}
--------------------------------------------------------------------------------
/templates/things/thing_list.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% load static %}
3 |
4 | {% block content %}
5 |
6 |
7 |
8 | All the Things
9 |
11 | {% for thing in things %}
12 |
14 | {{ thing.name }}
15 |
16 | {% endfor %}
17 |
18 |
19 |
20 |
21 | {% endblock content %}
22 |
--------------------------------------------------------------------------------
/templates/things/thing_update.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block content %}
4 | {# Update Thing
#}
5 | {# #}
10 |
11 |
13 |
53 |
54 | {% endblock content %}
55 |
--------------------------------------------------------------------------------
/things/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefellows/python-401-api-quickstart/aa02dd40ebaf766ed53cc41ed68cb70c2afd5e63/things/__init__.py
--------------------------------------------------------------------------------
/things/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from .models import Thing
3 |
4 | # Register your models here.
5 | admin.site.register(Thing)
6 |
--------------------------------------------------------------------------------
/things/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class ThingsConfig(AppConfig):
5 | name = 'things'
6 |
--------------------------------------------------------------------------------
/things/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codefellows/python-401-api-quickstart/aa02dd40ebaf766ed53cc41ed68cb70c2afd5e63/things/migrations/__init__.py
--------------------------------------------------------------------------------
/things/models.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth import get_user_model
2 | from django.db import models
3 | from django.urls import reverse
4 |
5 |
6 | class Thing(models.Model):
7 | name = models.CharField(max_length=256)
8 | rating = models.IntegerField(default=0, blank=True)
9 | reviewer = models.ForeignKey(
10 | get_user_model(), on_delete=models.CASCADE, null=True, blank=True
11 | )
12 | description = models.TextField(default="", null=True, blank=True)
13 |
14 | def __str__(self):
15 | return self.name
16 |
17 | def get_absolute_url(self):
18 | return reverse('thing_detail', args=[str(self.id)])
19 |
20 |
--------------------------------------------------------------------------------
/things/permissions.py:
--------------------------------------------------------------------------------
1 | from rest_framework import permissions
2 |
3 |
4 | class IsOwnerOrReadOnly(permissions.BasePermission):
5 | def has_object_permission(self, request, view, obj):
6 |
7 | # hover over SAFE_METHODS to see which qualify
8 | if request.method in permissions.SAFE_METHODS:
9 | return True
10 |
11 | # if we're allowing the purchaser to be null in Model
12 | # then this will check for that case and allow access
13 | if obj.owner is None:
14 | return True
15 |
16 | return obj.owner == request.user
17 |
--------------------------------------------------------------------------------
/things/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 | from .models import Thing
3 |
4 |
5 | class ThingSerializer(serializers.ModelSerializer):
6 | class Meta:
7 | model = Thing
8 | fields = "__all__"
9 |
--------------------------------------------------------------------------------
/things/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
5 | class ThingsTests(TestCase):
6 | # TODO: test your app
7 | def test_your_app(self):
8 | self.assertEqual("I have many tests", "I have no tests")
9 |
--------------------------------------------------------------------------------
/things/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from .views import ThingList, ThingDetail
3 |
4 | urlpatterns = [
5 | path("", ThingList.as_view(), name="thing_list"),
6 | path("/", ThingDetail.as_view(), name="thing_detail"),
7 | ]
8 |
--------------------------------------------------------------------------------
/things/urls_front.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from .views_front import (
3 | ThingCreateView,
4 | ThingDeleteView,
5 | ThingDetailView,
6 | ThingListView,
7 | ThingUpdateView,
8 | )
9 |
10 | urlpatterns = [
11 | path("", ThingListView.as_view(), name="thing_list"),
12 | path("/", ThingDetailView.as_view(), name="thing_detail"),
13 | path("create/", ThingCreateView.as_view(), name="thing_create"),
14 | path("/update/", ThingUpdateView.as_view(), name="thing_update"),
15 | path("/delete/", ThingDeleteView.as_view(), name="thing_delete"),
16 | ]
17 |
--------------------------------------------------------------------------------
/things/views.py:
--------------------------------------------------------------------------------
1 | from rest_framework.generics import (
2 | ListCreateAPIView,
3 | RetrieveUpdateDestroyAPIView,
4 | )
5 | from .models import Thing
6 | from .permissions import IsOwnerOrReadOnly
7 | from .serializers import ThingSerializer
8 |
9 |
10 | class ThingList(ListCreateAPIView):
11 | queryset = Thing.objects.all()
12 | serializer_class = ThingSerializer
13 |
14 |
15 | class ThingDetail(RetrieveUpdateDestroyAPIView):
16 | permission_classes = (IsOwnerOrReadOnly,)
17 | queryset = Thing.objects.all()
18 | serializer_class = ThingSerializer
19 |
--------------------------------------------------------------------------------
/things/views_front.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.mixins import LoginRequiredMixin
2 | from django.views.generic import ListView, DetailView, UpdateView, CreateView, DeleteView
3 | from django.urls import reverse_lazy
4 | from .models import Thing
5 |
6 |
7 | class ThingListView(LoginRequiredMixin, ListView):
8 | template_name = "things/thing_list.html"
9 | model = Thing
10 | context_object_name = "things"
11 |
12 |
13 | class ThingDetailView(LoginRequiredMixin, DetailView):
14 | template_name = "things/thing_detail.html"
15 | model = Thing
16 |
17 |
18 | class ThingUpdateView(LoginRequiredMixin, UpdateView):
19 | template_name = "things/thing_update.html"
20 | model = Thing
21 | fields = "__all__"
22 |
23 |
24 | class ThingCreateView(LoginRequiredMixin, CreateView):
25 | template_name = "things/thing_create.html"
26 | model = Thing
27 | fields = ["name", "rating", "reviewer"] # "__all__" for all of them
28 |
29 |
30 | class ThingDeleteView(LoginRequiredMixin, DeleteView):
31 | template_name = "things/thing_delete.html"
32 | model = Thing
33 | success_url = reverse_lazy("thing_list")
34 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "builds": [
3 | {
4 | "src": "project/wsgi.py",
5 | "use": "@vercel/python"
6 | }
7 | ],
8 | "routes": [
9 | {
10 | "src": "/(.*)",
11 | "dest": "project/wsgi.py"
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------