├── .flake8 ├── .gitconfig ├── .gitignore ├── .isort.cfg ├── .pre-commit-config.yaml ├── Procfile ├── README.md ├── build_docker_env.bat ├── build_docker_env.sh ├── docker-compose.yml ├── docker ├── django │ ├── Dockerfile │ └── entrypoint.sh └── jupyter │ ├── Dockerfile │ └── entrypoint.sh ├── pyproject.toml ├── requirements.txt ├── requirements ├── base.txt ├── development.txt └── production.txt ├── runtime.txt └── src ├── Lesson_02.04_manipulate_data.ipynb ├── Lesson_03.03_demonstrate_serializer_use.ipynb ├── Lesson_04.04_render_templates.ipynb ├── Lesson_04.06_create_urls_by_reversing_url_paths.ipynb ├── Lesson_05.01_save_serialized_data_to_database.ipynb ├── Lesson_05.02_update_database_via_serializer.ipynb ├── Lesson_05.05_save_data_via_startup_serializer.ipynb ├── Lesson_06.01_django_forms_in_python.ipynb ├── Lesson_06.02_model_validation.ipynb ├── blog ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── routers.py ├── serializers.py ├── tests.py ├── urls.py ├── views.py └── viewsets.py ├── config ├── __init__.py ├── settings │ ├── base.py │ ├── development.py │ └── production.py ├── urls.py ├── views.py └── wsgi.py ├── manage.py ├── organizer ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── routers.py ├── serializers.py ├── tests.py ├── urls.py ├── view_mixins.py ├── views.py └── viewsets.py └── templates ├── base.html ├── newslink ├── base.html ├── confirm_delete.html └── form.html ├── post ├── base.html ├── confirm_delete.html ├── detail.html ├── form.html └── list.html ├── root.html ├── startup ├── base.html ├── confirm_delete.html ├── detail.html ├── form.html └── list.html └── tag ├── base.html ├── confirm_delete.html ├── detail.html ├── form.html └── list.html /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude=settings.py, wsgi.py, manage.py, */migrations/* 3 | ignore = B950, D104, D105, D106, D400, E203, E266, E501, N803, N806, W503 4 | max-complexity = 10 5 | max-line-length = 60 6 | select = B, B9, C, D, E, F, N, W 7 | ignore-names = setUp, tearDown, setUpClass, tearDownClass, setUpTestData 8 | -------------------------------------------------------------------------------- /.gitconfig: -------------------------------------------------------------------------------- 1 | [alias] 2 | prev = checkout HEAD^ 3 | next = !git checkout `git rev-list HEAD..$(git branch --contains | tail -1) | tail -1` 4 | ci = commit 5 | co = checkout 6 | st = status 7 | ll = log --oneline 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .docker-env 2 | 3 | # https://github.com/github/gitignore/blob/master/Python.gitignore 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Environments 88 | .env 89 | .venv 90 | env/ 91 | venv/ 92 | ENV/ 93 | env.bak/ 94 | venv.bak/ 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # mkdocs documentation 104 | /site 105 | 106 | # mypy 107 | .mypy_cache/ 108 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | balanced_wrapping=true 3 | combine_as_imports=true 4 | force_add=true 5 | force_grid_wrap=0 6 | include_trailing_comma=true 7 | indent=' ' 8 | line_length=60 9 | multi_line_output=3 10 | not_skip=__init__.py 11 | known_third_party = django, django_extensions, environ, factory, faker, pytz, rest_framework, test_plus 12 | known_first_party = blog, config, contact, organizer, suorganizer 13 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.0.0 4 | hooks: 5 | - id: check-added-large-files 6 | args: [--maxkb=500] 7 | - id: check-byte-order-marker 8 | - id: check-case-conflict 9 | - id: check-json 10 | - id: check-merge-conflict 11 | - id: check-symlinks 12 | - id: debug-statements 13 | - id: detect-private-key 14 | - id: end-of-file-fixer 15 | - id: mixed-line-ending 16 | args: [--fix=lf] 17 | - id: requirements-txt-fixer 18 | - id: trailing-whitespace 19 | - repo: https://github.com/pre-commit/mirrors-isort 20 | rev: v4.3.4 21 | hooks: 22 | - id: isort 23 | - repo: https://github.com/ambv/black 24 | rev: 18.9b0 25 | hooks: 26 | - id: black 27 | language_version: python3.6 28 | - repo: local 29 | hooks: 30 | - id: flake8 31 | entry: flake8 32 | language: system 33 | name: flake8 34 | types: [python] 35 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | release: pip freeze && python src/manage.py migrate 2 | web: gunicorn config.wsgi --chdir src --log-file - 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Read Me 2 | 3 | This repository contains the code for the second class in [Andrew 4 | Pinkham]'s [Python Web Development] series, titled *Building Backend Web 5 | Applications and APIs with Django*. The series is published by Pearson 6 | and may be bought on [InformIT] or viewed on [Safari Books Online]. The 7 | series is for intermediate programmers new to web development or Django. 8 | 9 | [Andrew Pinkham]: https://andrewsforge.com 10 | [Python Web Development]: https://pywebdev.com 11 | [InformIT]: https://pywebdev.com/buy-21-2/ 12 | [Safari Books Online]: https://pywebdev.com/safari-21-2/ 13 | 14 | Andrew may be reached at [JamBon Software] for consulting and training. 15 | 16 | [JamBon Software]: https://www.jambonsw.com 17 | 18 | ## Table of Contents 19 | 20 | - [Changes Made Post-Recording](#changes-made-post-recording) 21 | - [Technical Requirements](#technical-requirements) 22 | - [Getting Started Instructions](#getting-started-instructions) 23 | - [Docker Setup](#docker-setup) 24 | - [Local Setup](#local-setup) 25 | - [Walking the Repository](#walking-the-Repository) 26 | - [Extra Problems](#extra-problems) 27 | - [Testing the Code](#testing-the-code) 28 | - [Deploying the Code](#deploying-the-code) 29 | 30 | ## Changes Made Post-Recording 31 | 32 | 1. For security purposes a commit has been added at the end of every 33 | branch which secures the application using basic login functionality. 34 | The content of that commit will be discussed in the third Python Web 35 | Development class - I hope you're looking forward to it! 36 | 2. Test dependencies have been included in all branches, so that the 37 | Docker image may be built once and used on all branches. 38 | 3. The Docker image has been updated to use Python 3.7.1 (from 3.7.0) 39 | 4. Pre-Commit and Black have been updated to more recent versions 40 | 5. The site uses Django's `StaticFilesStorage` instead of the 41 | `ManifestStaticFilesStorage` shown in Lesson 7 by error. 42 | 43 | [🔝 Up to Table of Contents](#table-of-contents) 44 | 45 | ## Technical Requirements 46 | 47 | - [Python] 3.6+ (with SQLite3 support) 48 | - [pip] 10+ 49 | - a virtual environment (e.g.: [`venv`], [`virtualenvwrapper`]) 50 | - Optional: 51 | - [Docker] 17.12+ with [Docker-Compose] (or—if unavailable—[PostgreSQL] 10) 52 | 53 | 54 | [Python]: https://www.python.org/downloads/ 55 | [pip]: https://pip.pypa.io/en/stable/installing/ 56 | [`venv`]:https://docs.python.org/3/library/venv.html 57 | [`virtualenvwrapper`]: https://virtualenvwrapper.readthedocs.io/en/latest/install.html 58 | [Docker]: https://www.docker.com/get-started 59 | [Docker-Compose]: https://docs.docker.com/compose/ 60 | [PostgreSQL]: https://www.postgresql.org/ 61 | 62 | All other technical requirements are installed by `pip` using the 63 | requirement files included in the repository. This includes [Django 2.1]. 64 | 65 | [Django 2.1]: https://docs.djangoproject.com/en/2.1/ 66 | 67 | [🔝 Up to Table of Contents](#table-of-contents) 68 | 69 | ## Getting Started Instructions 70 | 71 | For a full guide to using this code please refer to Lesson 2 of the 72 | class. This lesson demonstrates how to get started locally as well as 73 | how to use the Docker setup. 74 | 75 | If you are **unable to run Docker** on your machine skip to the [Local 76 | Setup](#local-setup) section. 77 | 78 | ### Docker Setup 79 | 80 | The use of Docker images allows us to avoid installing all of our 81 | dependencies—including PostgeSQL—locally. Furthermore, as discussed 82 | in the videos, it helps with parity between our development and 83 | production environments. 84 | 85 | Our Docker containers expect the existence of an environment file. To 86 | generate it on *nix systems please invoke the `build_docker_env.sh` 87 | script. 88 | 89 | ```shell 90 | ./build_docker_env.sh 91 | ``` 92 | 93 | On Windows please invoke the batch file. 94 | 95 | ``` 96 | build_docker_env 97 | ``` 98 | 99 | If you run into problems please refer to the videos for why we use this 100 | and what is needed in the event these scripts do not work. 101 | 102 | To run the Docker containers use the command below. 103 | 104 | ```shell 105 | docker-compose up 106 | ``` 107 | 108 | If you wish to run the servers in the background use the `-d` 109 | (**d**etached) flag, as demonstrated below. 110 | 111 | ```shell 112 | docker-compose up -d 113 | ``` 114 | 115 | To turn off the server use Control-C in the terminal window. If running 116 | in the background use the command below. 117 | 118 | ```shell 119 | docker-compose down 120 | ``` 121 | 122 | To remove all of the assets created by Docker to run the server use the 123 | command below. 124 | 125 | ```shell 126 | docker-compose down --volumes --rmi local 127 | ``` 128 | 129 | The `--volumes` flag may be shortened to `-v`. 130 | 131 | [🔝 Up to Table of Contents](#table-of-contents) 132 | 133 | ### Local Setup 134 | 135 | Use `pip` to install your development dependencies. 136 | 137 | ```console 138 | $ python3 -m pip install -r requirements/development.txt 139 | ``` 140 | 141 | If you have checked out to an earlier part of the code note that you 142 | will need to use `requirements.txt` instead of 143 | `requirements/development.txt`. 144 | 145 | You will need to define the`SECRET_KEY` environment variable. If you 146 | would like to use PostgreSQL locally you will need to set 147 | `DATABASE_URL`. 148 | 149 | ```shell 150 | export SECRET_KEY=`head -c 75 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 50` 151 | # replace the variables in <> below 152 | export DATABASE_URL='postgres://:@:5432/' 153 | ``` 154 | 155 | Please be advised that if you are running code in Lesson 2 you should 156 | expect to see errors. Lesson 2 changes the database structure but 157 | avoids making migrations until the very last moment. What's more, 158 | database settings change in Lesson 2.8. Errors are therefore normal! 159 | 160 | [🔝 Up to Table of Contents](#table-of-contents) 161 | 162 | ## Walking the Repository 163 | 164 | To make perusing the code in this repository as simple as possible the 165 | project defines its own `.gitconfig` file with custom commands 166 | (aliases). 167 | 168 | To enable the commands you must first point your local git 169 | configuration at the file provided. Either of the two commands below 170 | should work. 171 | 172 | ```shell 173 | # relative path 174 | git config --local include.path "../.gitconfig" 175 | # absolute path - *nix only! 176 | git config --local include.path "`builtin pwd`/.gitconfig" 177 | ``` 178 | 179 | This will enable the following git commands: 180 | 181 | - `git next`: Move to the next example/commit 182 | - `git prev`: Move to the previous example/commit 183 | - `git ci`: shortcut for `git commit` 184 | - `git co`: shortcut for `git checkout` 185 | - `git st`: shortcut for `git status` 186 | - `git ll`: shortcut for `git log --oneline` 187 | 188 | These commands can be used on any of the two branches in this 189 | repository, which are listed below. 190 | 191 | - `class_material`: contains the code and material seen in the videos, 192 | as well as solutions to exercises mentioned in Lessons 5 and 6 (see 193 | section below to review). 194 | - `with_tests`: includes material above, as well as the tests used by 195 | me to verify the code works 196 | 197 | [🔝 Up to Table of Contents](#table-of-contents) 198 | 199 | ## Extra Problems 200 | 201 | At the end of Lessons 5 and 6 I leave you with several optional 202 | exercises. 203 | 204 | In Lesson 5: 205 | 206 | - Create new `NewsLink` objects in API via POST method (Consider 207 | [`ViewSets`] vs [`APIView`] subclasses) 208 | - Simplify `PostSerializer` with [`HyperlinkedRelatedField`] (as seen on 209 | `NewsLinkSerializer`) 210 | - Use [`ViewSets`] and Routers to simplify Post handling in API 211 | 212 | [`ViewSets`]: https://www.django-rest-framework.org/api-guide/viewsets/ 213 | [`APIView`]: http://www.cdrf.co/3.7/rest_framework.views/APIView.html 214 | [`HyperlinkedRelatedField`]: https://www.django-rest-framework.org/api-guide/relations/#hyperlinkedrelatedfield 215 | 216 | In Lesson 6: 217 | 218 | - Use [`CreateView`], [`UpdateView`], and [`DeleteView`] to create views 219 | for `Startup` and `Post` objects (using `StartupForm` and `PostForm`) 220 | - Create a view to create new `NewsLink` objects and associate them 221 | automatically with `Startup` objects 222 | - Expand this view to handle updating `NewsLink` objects 223 | - Allow for `NewsLink` objects to be deleted 224 | 225 | [`CreateView`]: http://ccbv.co.uk/projects/Django/2.0/django.views.generic.edit/CreateView/ 226 | [`UpdateView`]: http://ccbv.co.uk/projects/Django/2.0/django.views.generic.edit/UpdateView/ 227 | [`DeleteView`]: http://ccbv.co.uk/projects/Django/2.0/django.views.generic.edit/DeleteView/ 228 | 229 | 230 | Below are a few other tasks to test your knowledge. 231 | 232 | - Create links in the templates to enable easier navigation across the 233 | site 234 | - Create a welcome page for the API (see the use of [`DefaultRouter`]) 235 | 236 | [`DefaultRouter`]: https://www.django-rest-framework.org/api-guide/routers/#defaultrouter 237 | 238 | The solutions to all of the tasks above can be found in the 239 | `class_material` git branch or [on Github]. 240 | 241 | [on Github]: https://github.com/jambonrose/python-web-dev-21-2 242 | 243 | [🔝 Up to Table of Contents](#table-of-contents) 244 | 245 | ## Testing the Code 246 | 247 | All of the tests used to build the code can be found in the 248 | [`with_tests` branch] on Github. 249 | 250 | [`with_tests` branch]: https://github.com/jambonrose/python-web-dev-21-2/tree/with_tests 251 | 252 | The branch (mostly) emulates a Test Driven-Development approach: commits 253 | prefixed with `test` write a test that will fail, while commits with 254 | lesson numbers then fix the failing tests from previous `test` commits. 255 | 256 | To run the tests locally use `manage.py`. 257 | 258 | ```shell 259 | # from root of project 260 | cd src 261 | python3 manage.py test 262 | ``` 263 | 264 | Tests may also be run in Docker. 265 | 266 | ```shell 267 | # from root of project 268 | docker-compose run --rm django python manage.py test 269 | ``` 270 | 271 | Be advised that running tests in Lesson 2 is tricky. You will generally 272 | need to create migrations before being able to run the tests, and there 273 | are a few commits that break the project's ability to run tests (such as 274 | when changing the database settings). 275 | 276 | We will cover material about how to test Django in the next Python Web 277 | Development class. I hope you're looking forward to it! 278 | 279 | [🔝 Up to Table of Contents](#table-of-contents) 280 | 281 | ## Deploying the Code 282 | 283 | The project follows [12 Factor App] principles and is configured to be 284 | deployed to [Heroku]. 285 | 286 | To start you will need to [sign up for Heroku] unless you already have 287 | an account. Make sure you have installed the [Heroku CLI]. 288 | 289 | [12 Factor App]: https://12factor.net/ 290 | [Heroku]: https://www.heroku.com/ 291 | [sign up for Heroku]: https://signup.heroku.com/ 292 | [Heroku CLI]: https://devcenter.heroku.com/articles/heroku-cli#download-and-install 293 | 294 | The following instructions are for *nix systems, and will need to be 295 | adapted for Windows. 296 | 297 | Ensure your app is ready for deployment. 298 | 299 | ```shell 300 | docker-compose run --rm django python manage.py check --deploy --settings="config.settings.production" 301 | ``` 302 | 303 | From the command line create a new app. 304 | 305 | ```shell 306 | $ heroku create 307 | ``` 308 | 309 | Heroku will give your app a random name (such as 310 | `infinite-tundra-77435`). Assign this name to a variable to be able to use 311 | commands below. 312 | 313 | ```shell 314 | $ export APP='infinite-tundra-77435' # replace with your actual app name 315 | ``` 316 | 317 | Your git repository should now have a new remote branch named `heroku`. 318 | If it is missing you can add it manually. 319 | 320 | ```shell 321 | $ heroku git:remote -a $APP 322 | ``` 323 | 324 | Create a PostgreSQL database for your app. 325 | 326 | ```shell 327 | $ heroku addons:create -a "$APP" heroku-postgresql:hobby-dev 328 | ``` 329 | 330 | Configure your app to use production settings. 331 | 332 | ```shell 333 | $ heroku config:set -a "$APP" DJANGO_SETTINGS_MODULE='config.settings.production' 334 | $ heroku config:set -a "$APP" SECRET_KEY="$(head -c 75 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 50)" 335 | $ heroku config:set -a "$APP" PYTHONHASHSEED=random 336 | $ heroku config:set -a "$APP" WEB_CONCURRENCY=2 337 | ``` 338 | 339 | You may now deploy your app. 340 | 341 | ```shell 342 | $ git push heroku class_material:master 343 | $ # you can also deploy the other branch to Heroku using: 344 | $ git push heroku with_tests:master 345 | ``` 346 | 347 | To create a new user use the command below. 348 | 349 | ```shell 350 | $ heroku run -a $APP python src/manage.py createsuperuser 351 | ``` 352 | 353 | To access the remote shell (to create data on the fly) use the command 354 | below. 355 | 356 | ```shell 357 | $ heroku run -a $APP python src/manage.py shell_plus 358 | ``` 359 | 360 | To see the app online you may use the command below. 361 | 362 | ```shell 363 | $ heroku open -a $APP 364 | ``` 365 | 366 | For more about what we've just done please see Heroku's [Getting 367 | Started with Python] documentation. If Lesson 7 does not cover 368 | as much material as you'd like you may be interested in Chapter 29 of 369 | [Django Unleashed]. 370 | 371 | [Getting Started with Python]: https://devcenter.heroku.com/articles/getting-started-with-python#introduction 372 | [Django Unleashed]: https://django-unleashed.com/ 373 | 374 | [🔝 Up to Table of Contents](#table-of-contents) 375 | -------------------------------------------------------------------------------- /build_docker_env.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set PG_DB=webdev21videos2 4 | set PG_PASSWORD=%RANDOM%%RANDOM%%RANDOM% 5 | set PG_SERVICE_NAME=postgres 6 | set PG_USER=webdev21videos2_user 7 | set SKEY=%RANDOM%%RANDOM%%RANDOM%%RANDOM%%RANDOM%%RANDOM% 8 | 9 | Echo POSTGRES_DB=%PG_DB% > .docker-env 10 | Echo POSTGRES_PASSWORD=%PG_PASSWORD% >> .docker-env 11 | Echo POSTGRES_USER=%PG_USER% >> .docker-env 12 | Echo DATABASE_URL=postgres://%PG_USER%:%PG_PASSWORD%@%PG_SERVICE_NAME%:5432/%PG_DB% >> .docker-env 13 | Echo SECRET_KEY=%SKEY% >> .docker-env 14 | -------------------------------------------------------------------------------- /build_docker_env.sh: -------------------------------------------------------------------------------- 1 | #/usr/bin/env sh 2 | 3 | PG_DB=webdev21videos2 4 | PG_PASSWORD=`head -c 18 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 12` 5 | PG_SERVICE_NAME=postgres 6 | PG_USER=webdev21videos2_user 7 | 8 | echo "POSTGRES_DB=$PG_DB 9 | POSTGRES_PASSWORD=$PG_PASSWORD 10 | POSTGRES_USER=$PG_USER 11 | DATABASE_URL=postgres://$PG_USER:$PG_PASSWORD@$PG_SERVICE_NAME:5432/$PG_DB 12 | SECRET_KEY=`head -c 75 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 50`" > .docker-env 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | volumes: 4 | postgres_data_dev: {} 5 | postgres_backup_dev: {} 6 | 7 | services: 8 | postgres: 9 | image: postgres:10.4-alpine 10 | env_file: ./.docker-env 11 | volumes: 12 | - postgres_data_dev:/var/lib/postgresql/data 13 | - postgres_backup_dev:/backups 14 | 15 | django: 16 | init: true 17 | build: 18 | context: . 19 | dockerfile: ./docker/django/Dockerfile 20 | depends_on: 21 | - postgres 22 | env_file: ./.docker-env 23 | ports: 24 | - "8000:8000" 25 | volumes: 26 | - ./src:/app 27 | 28 | jupyter: 29 | init: true 30 | build: 31 | context: . 32 | dockerfile: ./docker/jupyter/Dockerfile 33 | depends_on: 34 | - postgres 35 | env_file: ./.docker-env 36 | ports: 37 | - "8888:8888" 38 | volumes: 39 | - ./src:/app 40 | -------------------------------------------------------------------------------- /docker/django/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM revolutionsystems/python:3.7.1-wee-optimized-lto 2 | 3 | WORKDIR /app 4 | 5 | ENV PYTHONUNBUFFERED 1 6 | RUN python3.7 -m pip install -U pip setuptools 7 | 8 | COPY requirements /tmp/requirements 9 | RUN python3.7 -m pip install -U --no-cache-dir -r /tmp/requirements/development.txt 10 | 11 | COPY docker/django/entrypoint.sh /usr/local/bin/entrypoint.sh 12 | RUN chmod +x /usr/local/bin/entrypoint.sh 13 | ENTRYPOINT ["entrypoint.sh"] 14 | -------------------------------------------------------------------------------- /docker/django/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | if [ "$#" = 0 ] 4 | then 5 | python3.7 -m pip freeze 6 | fi 7 | 8 | postgres_ready() { 9 | python3.7 << END 10 | from sys import exit 11 | from psycopg2 import connect, OperationalError 12 | try: 13 | connect( 14 | dbname="$POSTGRES_DB", 15 | user="$POSTGRES_USER", 16 | password="$POSTGRES_PASSWORD", 17 | host="postgres", 18 | ) 19 | except OperationalError as error: 20 | print(error) 21 | exit(-1) 22 | exit(0) 23 | END 24 | } 25 | 26 | until postgres_ready; do 27 | >&2 echo "Postgres is unavailable - sleeping" 28 | sleep 3 29 | done; 30 | 31 | >&2 echo "Postgres is available" 32 | 33 | if [ "$#" = 0 ] 34 | then 35 | >&2 echo "No command detected; running default commands" 36 | >&2 echo "Running migrations" 37 | python3.7 manage.py migrate --noinput 38 | >&2 echo "\n\nStarting development server: 127.0.0.1:8000\n\n" 39 | python3.7 manage.py runserver 0.0.0.0:8000 40 | else 41 | >&2 echo "Command detected; running command" 42 | exec "$@" 43 | fi 44 | -------------------------------------------------------------------------------- /docker/jupyter/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM revolutionsystems/python:3.7.1-wee-optimized-lto 2 | 3 | WORKDIR /app 4 | 5 | ENV PYTHONUNBUFFERED 1 6 | RUN python3.7 -m pip install -U pip setuptools 7 | 8 | COPY requirements /tmp/requirements 9 | RUN python3.7 -m pip install -U --no-cache-dir -r /tmp/requirements/development.txt 10 | 11 | COPY docker/jupyter/entrypoint.sh /usr/local/bin/entrypoint.sh 12 | RUN chmod +x /usr/local/bin/entrypoint.sh 13 | ENTRYPOINT ["entrypoint.sh"] 14 | -------------------------------------------------------------------------------- /docker/jupyter/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | postgres_ready() { 4 | python3.7 << END 5 | from sys import exit 6 | from psycopg2 import connect, OperationalError 7 | try: 8 | connect( 9 | dbname="$POSTGRES_DB", 10 | user="$POSTGRES_USER", 11 | password="$POSTGRES_PASSWORD", 12 | host="postgres", 13 | ) 14 | except OperationalError as error: 15 | print(error) 16 | exit(-1) 17 | exit(0) 18 | END 19 | } 20 | 21 | until postgres_ready; do 22 | >&2 echo "Postgres is unavailable - sleeping" 23 | sleep 3 24 | done; 25 | 26 | >&2 echo "Postgres is available" 27 | 28 | python3.7 manage.py shell_plus --notebook 29 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 60 3 | py36 = true 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements/production.txt 2 | -------------------------------------------------------------------------------- /requirements/base.txt: -------------------------------------------------------------------------------- 1 | django-environ==0.4.5 2 | django-extensions==2.1.0 3 | django-url-checks==0.1.0 4 | Django>=2.1,<2.2 5 | djangorestframework==3.8.2 6 | ipython==6.4.0 7 | pytz==2018.5 8 | whitenoise==4.1 9 | -------------------------------------------------------------------------------- /requirements/development.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | 3 | django-test-plus==1.1.1 4 | factory_boy==2.11.1 5 | Faker==0.9.2 6 | flake8==3.5.0 7 | flake8-bugbear==18.2.0 8 | flake8-docstrings==1.3.0 9 | jupyter==1.0.0 10 | pep8-naming==0.7.0 11 | pre-commit==1.10.4 12 | psycopg2-binary==2.7.5 13 | -------------------------------------------------------------------------------- /requirements/production.txt: -------------------------------------------------------------------------------- 1 | -r base.txt 2 | 3 | Brotli==1.0.4 4 | gunicorn==19.7.1 5 | psycopg2>=2.7,<2.8 --no-binary psycopg2 6 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.7.0 2 | -------------------------------------------------------------------------------- /src/Lesson_02.04_manipulate_data.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Preparation\n", 8 | "\n", 9 | "This code is meant to be used during Lesson 2.5 of Andrew Pinkham's _Building backend web applications and APIs with Django_ class. \n", 10 | "\n", 11 | "To re-run this code you need a working *empty* database. The results of this notebook may vary depending on where in the git history you have navigated to, and whether you have made your own changes to the code.\n", 12 | "\n", 13 | "Consider that if you have an SQLite3 or PostgreSQL database with data in it, running this code will modify the data." 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": 1, 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "from datetime import date\n", 23 | "\n", 24 | "from blog.models import Post\n", 25 | "from organizer.models import Tag, Startup, NewsLink" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": {}, 31 | "source": [ 32 | "# Basic Interaction with Django Models" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": 2, 38 | "metadata": {}, 39 | "outputs": [], 40 | "source": [ 41 | "edut = Tag(name='Education', slug='education')" 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": 3, 47 | "metadata": {}, 48 | "outputs": [ 49 | { 50 | "data": { 51 | "text/plain": [ 52 | "" 53 | ] 54 | }, 55 | "execution_count": 3, 56 | "metadata": {}, 57 | "output_type": "execute_result" 58 | } 59 | ], 60 | "source": [ 61 | "edut" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": 4, 67 | "metadata": {}, 68 | "outputs": [ 69 | { 70 | "data": { 71 | "text/plain": [ 72 | "'Education'" 73 | ] 74 | }, 75 | "execution_count": 4, 76 | "metadata": {}, 77 | "output_type": "execute_result" 78 | } 79 | ], 80 | "source": [ 81 | "edut.name" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": 5, 87 | "metadata": { 88 | "scrolled": false 89 | }, 90 | "outputs": [], 91 | "source": [ 92 | "edut.save() # saves the data in the model to the database!" 93 | ] 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": 6, 98 | "metadata": {}, 99 | "outputs": [ 100 | { 101 | "data": { 102 | "text/plain": [ 103 | "(1, {'blog.Post_tags': 0, 'organizer.Startup_tags': 0, 'organizer.Tag': 1})" 104 | ] 105 | }, 106 | "execution_count": 6, 107 | "metadata": {}, 108 | "output_type": "execute_result" 109 | } 110 | ], 111 | "source": [ 112 | "edut.delete() # deleted the data from the database!" 113 | ] 114 | }, 115 | { 116 | "cell_type": "code", 117 | "execution_count": 7, 118 | "metadata": { 119 | "scrolled": true 120 | }, 121 | "outputs": [ 122 | { 123 | "data": { 124 | "text/plain": [ 125 | "" 126 | ] 127 | }, 128 | "execution_count": 7, 129 | "metadata": {}, 130 | "output_type": "execute_result" 131 | } 132 | ], 133 | "source": [ 134 | "edut # still in memory!" 135 | ] 136 | }, 137 | { 138 | "cell_type": "markdown", 139 | "metadata": {}, 140 | "source": [ 141 | "## Creation and Destruction with Managers" 142 | ] 143 | }, 144 | { 145 | "cell_type": "code", 146 | "execution_count": 8, 147 | "metadata": {}, 148 | "outputs": [ 149 | { 150 | "data": { 151 | "text/plain": [ 152 | "django.db.models.manager.Manager" 153 | ] 154 | }, 155 | "execution_count": 8, 156 | "metadata": {}, 157 | "output_type": "execute_result" 158 | } 159 | ], 160 | "source": [ 161 | "type(Tag.objects) # a model manager" 162 | ] 163 | }, 164 | { 165 | "cell_type": "code", 166 | "execution_count": 9, 167 | "metadata": {}, 168 | "outputs": [ 169 | { 170 | "data": { 171 | "text/plain": [ 172 | "" 173 | ] 174 | }, 175 | "execution_count": 9, 176 | "metadata": {}, 177 | "output_type": "execute_result" 178 | } 179 | ], 180 | "source": [ 181 | "Tag.objects.create(name='Video Games', slug='video-games')" 182 | ] 183 | }, 184 | { 185 | "cell_type": "code", 186 | "execution_count": 10, 187 | "metadata": { 188 | "scrolled": true 189 | }, 190 | "outputs": [ 191 | { 192 | "data": { 193 | "text/plain": [ 194 | "[, , ]" 195 | ] 196 | }, 197 | "execution_count": 10, 198 | "metadata": {}, 199 | "output_type": "execute_result" 200 | } 201 | ], 202 | "source": [ 203 | "# create multiple objects in a go!\n", 204 | "Tag.objects.bulk_create([\n", 205 | " Tag(name='Django', slug='django'),\n", 206 | " Tag(name='Mobile', slug='mobile'),\n", 207 | " Tag(name='Web', slug='web'),\n", 208 | "])" 209 | ] 210 | }, 211 | { 212 | "cell_type": "code", 213 | "execution_count": 11, 214 | "metadata": { 215 | "scrolled": true 216 | }, 217 | "outputs": [ 218 | { 219 | "data": { 220 | "text/plain": [ 221 | ", , , ]>" 222 | ] 223 | }, 224 | "execution_count": 11, 225 | "metadata": {}, 226 | "output_type": "execute_result" 227 | } 228 | ], 229 | "source": [ 230 | "Tag.objects.all()" 231 | ] 232 | }, 233 | { 234 | "cell_type": "code", 235 | "execution_count": 12, 236 | "metadata": {}, 237 | "outputs": [ 238 | { 239 | "data": { 240 | "text/plain": [ 241 | "" 242 | ] 243 | }, 244 | "execution_count": 12, 245 | "metadata": {}, 246 | "output_type": "execute_result" 247 | } 248 | ], 249 | "source": [ 250 | "Tag.objects.all()[0] # acts like a list" 251 | ] 252 | }, 253 | { 254 | "cell_type": "code", 255 | "execution_count": 13, 256 | "metadata": {}, 257 | "outputs": [ 258 | { 259 | "data": { 260 | "text/plain": [ 261 | "django.db.models.query.QuerySet" 262 | ] 263 | }, 264 | "execution_count": 13, 265 | "metadata": {}, 266 | "output_type": "execute_result" 267 | } 268 | ], 269 | "source": [ 270 | "type(Tag.objects.all()) # is not a list" 271 | ] 272 | }, 273 | { 274 | "cell_type": "code", 275 | "execution_count": 14, 276 | "metadata": { 277 | "scrolled": true 278 | }, 279 | "outputs": [ 280 | { 281 | "name": "stdout", 282 | "output_type": "stream", 283 | "text": [ 284 | "Manager isn't accessible via Tag instances\n" 285 | ] 286 | } 287 | ], 288 | "source": [ 289 | "# managers are not accessible to model instances, only to model classes!\n", 290 | "try:\n", 291 | " edut.objects\n", 292 | "except AttributeError as e:\n", 293 | " print(e)" 294 | ] 295 | }, 296 | { 297 | "cell_type": "markdown", 298 | "metadata": {}, 299 | "source": [ 300 | "## Methods of Data Retrieval" 301 | ] 302 | }, 303 | { 304 | "cell_type": "code", 305 | "execution_count": 15, 306 | "metadata": {}, 307 | "outputs": [ 308 | { 309 | "data": { 310 | "text/plain": [ 311 | ", , , ]>" 312 | ] 313 | }, 314 | "execution_count": 15, 315 | "metadata": {}, 316 | "output_type": "execute_result" 317 | } 318 | ], 319 | "source": [ 320 | "Tag.objects.all()" 321 | ] 322 | }, 323 | { 324 | "cell_type": "code", 325 | "execution_count": 16, 326 | "metadata": {}, 327 | "outputs": [ 328 | { 329 | "data": { 330 | "text/plain": [ 331 | "4" 332 | ] 333 | }, 334 | "execution_count": 16, 335 | "metadata": {}, 336 | "output_type": "execute_result" 337 | } 338 | ], 339 | "source": [ 340 | "Tag.objects.count()" 341 | ] 342 | }, 343 | { 344 | "cell_type": "markdown", 345 | "metadata": {}, 346 | "source": [ 347 | "### The `get` method" 348 | ] 349 | }, 350 | { 351 | "cell_type": "code", 352 | "execution_count": 17, 353 | "metadata": {}, 354 | "outputs": [ 355 | { 356 | "data": { 357 | "text/plain": [ 358 | "" 359 | ] 360 | }, 361 | "execution_count": 17, 362 | "metadata": {}, 363 | "output_type": "execute_result" 364 | } 365 | ], 366 | "source": [ 367 | "Tag.objects.get(slug='django')" 368 | ] 369 | }, 370 | { 371 | "cell_type": "code", 372 | "execution_count": 18, 373 | "metadata": {}, 374 | "outputs": [ 375 | { 376 | "data": { 377 | "text/plain": [ 378 | "django.db.models.query.QuerySet" 379 | ] 380 | }, 381 | "execution_count": 18, 382 | "metadata": {}, 383 | "output_type": "execute_result" 384 | } 385 | ], 386 | "source": [ 387 | "type(Tag.objects.all())" 388 | ] 389 | }, 390 | { 391 | "cell_type": "code", 392 | "execution_count": 19, 393 | "metadata": {}, 394 | "outputs": [ 395 | { 396 | "data": { 397 | "text/plain": [ 398 | "organizer.models.Tag" 399 | ] 400 | }, 401 | "execution_count": 19, 402 | "metadata": {}, 403 | "output_type": "execute_result" 404 | } 405 | ], 406 | "source": [ 407 | "type(Tag.objects.get(slug='django'))" 408 | ] 409 | }, 410 | { 411 | "cell_type": "code", 412 | "execution_count": 20, 413 | "metadata": {}, 414 | "outputs": [ 415 | { 416 | "name": "stdout", 417 | "output_type": "stream", 418 | "text": [ 419 | "Tag matching query does not exist.\n" 420 | ] 421 | } 422 | ], 423 | "source": [ 424 | "# case-sensitive!\n", 425 | "try:\n", 426 | " Tag.objects.get(slug='Django')\n", 427 | "except Tag.DoesNotExist as e:\n", 428 | " print(e)" 429 | ] 430 | }, 431 | { 432 | "cell_type": "code", 433 | "execution_count": 21, 434 | "metadata": {}, 435 | "outputs": [ 436 | { 437 | "data": { 438 | "text/plain": [ 439 | "" 440 | ] 441 | }, 442 | "execution_count": 21, 443 | "metadata": {}, 444 | "output_type": "execute_result" 445 | } 446 | ], 447 | "source": [ 448 | "# the i is for case-Insensitive\n", 449 | "Tag.objects.get(slug__iexact='DJANGO')" 450 | ] 451 | }, 452 | { 453 | "cell_type": "code", 454 | "execution_count": 22, 455 | "metadata": {}, 456 | "outputs": [ 457 | { 458 | "data": { 459 | "text/plain": [ 460 | "" 461 | ] 462 | }, 463 | "execution_count": 22, 464 | "metadata": {}, 465 | "output_type": "execute_result" 466 | } 467 | ], 468 | "source": [ 469 | "Tag.objects.get(slug__istartswith='DJ')" 470 | ] 471 | }, 472 | { 473 | "cell_type": "code", 474 | "execution_count": 23, 475 | "metadata": {}, 476 | "outputs": [ 477 | { 478 | "data": { 479 | "text/plain": [ 480 | "" 481 | ] 482 | }, 483 | "execution_count": 23, 484 | "metadata": {}, 485 | "output_type": "execute_result" 486 | } 487 | ], 488 | "source": [ 489 | "Tag.objects.get(slug__contains='an')" 490 | ] 491 | }, 492 | { 493 | "cell_type": "code", 494 | "execution_count": 24, 495 | "metadata": { 496 | "scrolled": true 497 | }, 498 | "outputs": [ 499 | { 500 | "name": "stdout", 501 | "output_type": "stream", 502 | "text": [ 503 | "get() returned more than one Tag -- it returned 3!\n" 504 | ] 505 | } 506 | ], 507 | "source": [ 508 | "# get always returns a single object\n", 509 | "try:\n", 510 | " # djangO, mObile, videO-games\n", 511 | " Tag.objects.get(slug__contains='o')\n", 512 | "except Tag.MultipleObjectsReturned as e:\n", 513 | " print(e)" 514 | ] 515 | }, 516 | { 517 | "cell_type": "markdown", 518 | "metadata": {}, 519 | "source": [ 520 | "### The `filter` method" 521 | ] 522 | }, 523 | { 524 | "cell_type": "code", 525 | "execution_count": 25, 526 | "metadata": {}, 527 | "outputs": [ 528 | { 529 | "data": { 530 | "text/plain": [ 531 | ", , ]>" 532 | ] 533 | }, 534 | "execution_count": 25, 535 | "metadata": {}, 536 | "output_type": "execute_result" 537 | } 538 | ], 539 | "source": [ 540 | "## unlike get, can fetch multiple objects\n", 541 | "Tag.objects.filter(slug__contains='o')" 542 | ] 543 | }, 544 | { 545 | "cell_type": "code", 546 | "execution_count": 26, 547 | "metadata": {}, 548 | "outputs": [ 549 | { 550 | "data": { 551 | "text/plain": [ 552 | "django.db.models.query.QuerySet" 553 | ] 554 | }, 555 | "execution_count": 26, 556 | "metadata": {}, 557 | "output_type": "execute_result" 558 | } 559 | ], 560 | "source": [ 561 | "type(Tag.objects.filter(slug__contains='o'))" 562 | ] 563 | }, 564 | { 565 | "cell_type": "markdown", 566 | "metadata": {}, 567 | "source": [ 568 | "### Chaining Calls" 569 | ] 570 | }, 571 | { 572 | "cell_type": "code", 573 | "execution_count": 27, 574 | "metadata": {}, 575 | "outputs": [ 576 | { 577 | "data": { 578 | "text/plain": [ 579 | ", , ]>" 580 | ] 581 | }, 582 | "execution_count": 27, 583 | "metadata": {}, 584 | "output_type": "execute_result" 585 | } 586 | ], 587 | "source": [ 588 | "Tag.objects.filter(slug__contains='o').order_by('-name')" 589 | ] 590 | }, 591 | { 592 | "cell_type": "code", 593 | "execution_count": 28, 594 | "metadata": {}, 595 | "outputs": [ 596 | { 597 | "data": { 598 | "text/plain": [ 599 | ", , , ]>" 600 | ] 601 | }, 602 | "execution_count": 28, 603 | "metadata": {}, 604 | "output_type": "execute_result" 605 | } 606 | ], 607 | "source": [ 608 | "# first we call order_by on the manager\n", 609 | "Tag.objects.order_by('-name')" 610 | ] 611 | }, 612 | { 613 | "cell_type": "code", 614 | "execution_count": 29, 615 | "metadata": {}, 616 | "outputs": [ 617 | { 618 | "data": { 619 | "text/plain": [ 620 | ", , ]>" 621 | ] 622 | }, 623 | "execution_count": 29, 624 | "metadata": {}, 625 | "output_type": "execute_result" 626 | } 627 | ], 628 | "source": [ 629 | "# now we call filter on the manager, and order the resulting queryset\n", 630 | "Tag.objects.filter(slug__contains='e').order_by('-name')" 631 | ] 632 | }, 633 | { 634 | "cell_type": "markdown", 635 | "metadata": {}, 636 | "source": [ 637 | "### `values` and `values_list`" 638 | ] 639 | }, 640 | { 641 | "cell_type": "code", 642 | "execution_count": 30, 643 | "metadata": {}, 644 | "outputs": [ 645 | { 646 | "data": { 647 | "text/plain": [ 648 | "" 649 | ] 650 | }, 651 | "execution_count": 30, 652 | "metadata": {}, 653 | "output_type": "execute_result" 654 | } 655 | ], 656 | "source": [ 657 | "Tag.objects.values_list()" 658 | ] 659 | }, 660 | { 661 | "cell_type": "code", 662 | "execution_count": 31, 663 | "metadata": {}, 664 | "outputs": [ 665 | { 666 | "data": { 667 | "text/plain": [ 668 | "django.db.models.query.QuerySet" 669 | ] 670 | }, 671 | "execution_count": 31, 672 | "metadata": {}, 673 | "output_type": "execute_result" 674 | } 675 | ], 676 | "source": [ 677 | "type(Tag.objects.values_list())" 678 | ] 679 | }, 680 | { 681 | "cell_type": "code", 682 | "execution_count": 32, 683 | "metadata": {}, 684 | "outputs": [ 685 | { 686 | "data": { 687 | "text/plain": [ 688 | "" 689 | ] 690 | }, 691 | "execution_count": 32, 692 | "metadata": {}, 693 | "output_type": "execute_result" 694 | } 695 | ], 696 | "source": [ 697 | "Tag.objects.values_list('name', 'slug')" 698 | ] 699 | }, 700 | { 701 | "cell_type": "code", 702 | "execution_count": 33, 703 | "metadata": {}, 704 | "outputs": [ 705 | { 706 | "data": { 707 | "text/plain": [ 708 | "" 709 | ] 710 | }, 711 | "execution_count": 33, 712 | "metadata": {}, 713 | "output_type": "execute_result" 714 | } 715 | ], 716 | "source": [ 717 | "Tag.objects.values_list('name')" 718 | ] 719 | }, 720 | { 721 | "cell_type": "code", 722 | "execution_count": 34, 723 | "metadata": {}, 724 | "outputs": [ 725 | { 726 | "data": { 727 | "text/plain": [ 728 | "" 729 | ] 730 | }, 731 | "execution_count": 34, 732 | "metadata": {}, 733 | "output_type": "execute_result" 734 | } 735 | ], 736 | "source": [ 737 | "Tag.objects.values_list('name', flat=True)" 738 | ] 739 | }, 740 | { 741 | "cell_type": "code", 742 | "execution_count": 35, 743 | "metadata": {}, 744 | "outputs": [ 745 | { 746 | "data": { 747 | "text/plain": [ 748 | "django.db.models.query.QuerySet" 749 | ] 750 | }, 751 | "execution_count": 35, 752 | "metadata": {}, 753 | "output_type": "execute_result" 754 | } 755 | ], 756 | "source": [ 757 | "type(Tag.objects.values_list('name', flat=True))" 758 | ] 759 | }, 760 | { 761 | "cell_type": "markdown", 762 | "metadata": {}, 763 | "source": [ 764 | "## Data in Memory vs Data in the Database" 765 | ] 766 | }, 767 | { 768 | "cell_type": "code", 769 | "execution_count": 36, 770 | "metadata": {}, 771 | "outputs": [ 772 | { 773 | "data": { 774 | "text/plain": [ 775 | "" 776 | ] 777 | }, 778 | "execution_count": 36, 779 | "metadata": {}, 780 | "output_type": "execute_result" 781 | } 782 | ], 783 | "source": [ 784 | "jb = Startup.objects.create(\n", 785 | " name='JamBon Software',\n", 786 | " slug='jambon-software',\n", 787 | " contact='django@jambonsw.com',\n", 788 | " description='Web and Mobile Consulting.\\n'\n", 789 | " 'Django Tutoring.\\n',\n", 790 | " founded_date=date(2013, 1, 18),\n", 791 | " website='https://jambonsw.com/',\n", 792 | ")\n", 793 | "jb # this output only clear because of __str__()" 794 | ] 795 | }, 796 | { 797 | "cell_type": "code", 798 | "execution_count": 37, 799 | "metadata": {}, 800 | "outputs": [ 801 | { 802 | "data": { 803 | "text/plain": [ 804 | "datetime.date(2013, 1, 18)" 805 | ] 806 | }, 807 | "execution_count": 37, 808 | "metadata": {}, 809 | "output_type": "execute_result" 810 | } 811 | ], 812 | "source": [ 813 | "jb.founded_date" 814 | ] 815 | }, 816 | { 817 | "cell_type": "code", 818 | "execution_count": 38, 819 | "metadata": {}, 820 | "outputs": [ 821 | { 822 | "data": { 823 | "text/plain": [ 824 | "datetime.date(2014, 1, 1)" 825 | ] 826 | }, 827 | "execution_count": 38, 828 | "metadata": {}, 829 | "output_type": "execute_result" 830 | } 831 | ], 832 | "source": [ 833 | "jb.founded_date = date(2014,1,1)\n", 834 | "# we're not calling save() !\n", 835 | "jb.founded_date" 836 | ] 837 | }, 838 | { 839 | "cell_type": "code", 840 | "execution_count": 39, 841 | "metadata": {}, 842 | "outputs": [ 843 | { 844 | "data": { 845 | "text/plain": [ 846 | "datetime.date(2013, 1, 18)" 847 | ] 848 | }, 849 | "execution_count": 39, 850 | "metadata": {}, 851 | "output_type": "execute_result" 852 | } 853 | ], 854 | "source": [ 855 | "# get version in database\n", 856 | "jb = Startup.objects.get(slug='jambon-software')\n", 857 | "# work above is all for nought because we didn't save()\n", 858 | "jb.founded_date" 859 | ] 860 | }, 861 | { 862 | "cell_type": "markdown", 863 | "metadata": { 864 | "collapsed": true 865 | }, 866 | "source": [ 867 | "## Connecting Data through Relations" 868 | ] 869 | }, 870 | { 871 | "cell_type": "code", 872 | "execution_count": 40, 873 | "metadata": {}, 874 | "outputs": [ 875 | { 876 | "data": { 877 | "text/plain": [ 878 | "" 879 | ] 880 | }, 881 | "execution_count": 40, 882 | "metadata": {}, 883 | "output_type": "execute_result" 884 | } 885 | ], 886 | "source": [ 887 | "djt = Post.objects.create(\n", 888 | " title='Django Training',\n", 889 | " slug='django-training',\n", 890 | " text=(\n", 891 | " \"Learn Django in a classroom setting \"\n", 892 | " \"with JamBon Software.\"),\n", 893 | ")\n", 894 | "djt" 895 | ] 896 | }, 897 | { 898 | "cell_type": "code", 899 | "execution_count": 41, 900 | "metadata": {}, 901 | "outputs": [ 902 | { 903 | "data": { 904 | "text/plain": [ 905 | "" 906 | ] 907 | }, 908 | "execution_count": 41, 909 | "metadata": {}, 910 | "output_type": "execute_result" 911 | } 912 | ], 913 | "source": [ 914 | "djt.pub_date = date(2013, 1, 18)\n", 915 | "djt.save()\n", 916 | "djt" 917 | ] 918 | }, 919 | { 920 | "cell_type": "code", 921 | "execution_count": 42, 922 | "metadata": {}, 923 | "outputs": [ 924 | { 925 | "data": { 926 | "text/plain": [ 927 | "django.db.models.fields.related_descriptors.create_forward_many_to_many_manager..ManyRelatedManager" 928 | ] 929 | }, 930 | "execution_count": 42, 931 | "metadata": {}, 932 | "output_type": "execute_result" 933 | } 934 | ], 935 | "source": [ 936 | "type(djt.tags)" 937 | ] 938 | }, 939 | { 940 | "cell_type": "code", 941 | "execution_count": 43, 942 | "metadata": {}, 943 | "outputs": [ 944 | { 945 | "data": { 946 | "text/plain": [ 947 | "django.db.models.fields.related_descriptors.create_forward_many_to_many_manager..ManyRelatedManager" 948 | ] 949 | }, 950 | "execution_count": 43, 951 | "metadata": {}, 952 | "output_type": "execute_result" 953 | } 954 | ], 955 | "source": [ 956 | "type(djt.startups)" 957 | ] 958 | }, 959 | { 960 | "cell_type": "code", 961 | "execution_count": 44, 962 | "metadata": {}, 963 | "outputs": [ 964 | { 965 | "data": { 966 | "text/plain": [ 967 | "" 968 | ] 969 | }, 970 | "execution_count": 44, 971 | "metadata": {}, 972 | "output_type": "execute_result" 973 | } 974 | ], 975 | "source": [ 976 | "djt.tags.all()" 977 | ] 978 | }, 979 | { 980 | "cell_type": "code", 981 | "execution_count": 45, 982 | "metadata": {}, 983 | "outputs": [ 984 | { 985 | "data": { 986 | "text/plain": [ 987 | "" 988 | ] 989 | }, 990 | "execution_count": 45, 991 | "metadata": {}, 992 | "output_type": "execute_result" 993 | } 994 | ], 995 | "source": [ 996 | "djt.startups.all()" 997 | ] 998 | }, 999 | { 1000 | "cell_type": "code", 1001 | "execution_count": 46, 1002 | "metadata": {}, 1003 | "outputs": [ 1004 | { 1005 | "data": { 1006 | "text/plain": [ 1007 | "]>" 1008 | ] 1009 | }, 1010 | "execution_count": 46, 1011 | "metadata": {}, 1012 | "output_type": "execute_result" 1013 | } 1014 | ], 1015 | "source": [ 1016 | "django = Tag.objects.get(slug__contains='django')\n", 1017 | "djt.tags.add(django)\n", 1018 | "djt.tags.all()" 1019 | ] 1020 | }, 1021 | { 1022 | "cell_type": "code", 1023 | "execution_count": 47, 1024 | "metadata": {}, 1025 | "outputs": [ 1026 | { 1027 | "data": { 1028 | "text/plain": [ 1029 | "]>" 1030 | ] 1031 | }, 1032 | "execution_count": 47, 1033 | "metadata": {}, 1034 | "output_type": "execute_result" 1035 | } 1036 | ], 1037 | "source": [ 1038 | "django.blog_posts.all() # a \"reverse\" relation" 1039 | ] 1040 | }, 1041 | { 1042 | "cell_type": "code", 1043 | "execution_count": 48, 1044 | "metadata": {}, 1045 | "outputs": [ 1046 | { 1047 | "data": { 1048 | "text/plain": [ 1049 | "]>" 1050 | ] 1051 | }, 1052 | "execution_count": 48, 1053 | "metadata": {}, 1054 | "output_type": "execute_result" 1055 | } 1056 | ], 1057 | "source": [ 1058 | "django.startup_set.add(jb) # a \"reverse\" relation\n", 1059 | "django.startup_set.all()" 1060 | ] 1061 | }, 1062 | { 1063 | "cell_type": "code", 1064 | "execution_count": 49, 1065 | "metadata": {}, 1066 | "outputs": [ 1067 | { 1068 | "data": { 1069 | "text/plain": [ 1070 | "]>" 1071 | ] 1072 | }, 1073 | "execution_count": 49, 1074 | "metadata": {}, 1075 | "output_type": "execute_result" 1076 | } 1077 | ], 1078 | "source": [ 1079 | "jb.tags.all() # the \"forward\" relation" 1080 | ] 1081 | }, 1082 | { 1083 | "cell_type": "code", 1084 | "execution_count": 50, 1085 | "metadata": {}, 1086 | "outputs": [ 1087 | { 1088 | "data": { 1089 | "text/plain": [ 1090 | "" 1091 | ] 1092 | }, 1093 | "execution_count": 50, 1094 | "metadata": {}, 1095 | "output_type": "execute_result" 1096 | } 1097 | ], 1098 | "source": [ 1099 | "# on more time, for repetition!\n", 1100 | "djt" 1101 | ] 1102 | }, 1103 | { 1104 | "cell_type": "code", 1105 | "execution_count": 51, 1106 | "metadata": {}, 1107 | "outputs": [ 1108 | { 1109 | "data": { 1110 | "text/plain": [ 1111 | "]>" 1112 | ] 1113 | }, 1114 | "execution_count": 51, 1115 | "metadata": {}, 1116 | "output_type": "execute_result" 1117 | } 1118 | ], 1119 | "source": [ 1120 | "# \"forward\" relation\n", 1121 | "djt.startups.add(jb)\n", 1122 | "djt.startups.all()" 1123 | ] 1124 | }, 1125 | { 1126 | "cell_type": "code", 1127 | "execution_count": 52, 1128 | "metadata": {}, 1129 | "outputs": [ 1130 | { 1131 | "data": { 1132 | "text/plain": [ 1133 | "]>" 1134 | ] 1135 | }, 1136 | "execution_count": 52, 1137 | "metadata": {}, 1138 | "output_type": "execute_result" 1139 | } 1140 | ], 1141 | "source": [ 1142 | "jb.blog_posts.all() # \"reverse\" relation" 1143 | ] 1144 | } 1145 | ], 1146 | "metadata": { 1147 | "kernelspec": { 1148 | "display_name": "Django Shell-Plus", 1149 | "language": "python", 1150 | "name": "django_extensions" 1151 | }, 1152 | "language_info": { 1153 | "codemirror_mode": { 1154 | "name": "ipython", 1155 | "version": 3 1156 | }, 1157 | "file_extension": ".py", 1158 | "mimetype": "text/x-python", 1159 | "name": "python", 1160 | "nbconvert_exporter": "python", 1161 | "pygments_lexer": "ipython3", 1162 | "version": "3.6.6" 1163 | } 1164 | }, 1165 | "nbformat": 4, 1166 | "nbformat_minor": 1 1167 | } 1168 | -------------------------------------------------------------------------------- /src/Lesson_03.03_demonstrate_serializer_use.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Serializer Demonstration\n", 8 | "\n", 9 | "This Jupyter notebook expects there to be a database available. Remember to run migrations!\n", 10 | "\n", 11 | "Additionally, `djangorestframework-xml` must be installed for this to work correctly. We do not use the package in the rest of the code, so manual installation is required.\n", 12 | "\n", 13 | " pip install 'djangorestframework-xml>=1.3.0' 'defusedxml>=0.5.0'" 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": 1, 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "from datetime import date\n", 23 | "from pprint import pprint\n", 24 | "from xml.dom.minidom import parseString as xml_parse\n", 25 | "\n", 26 | "from rest_framework.renderers import JSONRenderer\n", 27 | "from rest_framework_xml.renderers import XMLRenderer\n", 28 | "\n", 29 | "from organizer.models import NewsLink, Startup, Tag\n", 30 | "from organizer.serializers import NewsLinkSerializer, StartupSerializer, TagSerializer" 31 | ] 32 | }, 33 | { 34 | "cell_type": "markdown", 35 | "metadata": {}, 36 | "source": [ 37 | "## Simple Example with Tag Object" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 2, 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "new_tag = Tag.objects.create(name='django')" 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": 3, 52 | "metadata": {}, 53 | "outputs": [], 54 | "source": [ 55 | "s_tag = TagSerializer(new_tag)" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": 4, 61 | "metadata": {}, 62 | "outputs": [ 63 | { 64 | "data": { 65 | "text/plain": [ 66 | "TagSerializer():\n", 67 | " id = IntegerField(read_only=True)\n", 68 | " name = CharField(max_length=31)\n", 69 | " slug = SlugField(allow_blank=True, max_length=31)" 70 | ] 71 | }, 72 | "execution_count": 4, 73 | "metadata": {}, 74 | "output_type": "execute_result" 75 | } 76 | ], 77 | "source": [ 78 | "s_tag" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": 5, 84 | "metadata": { 85 | "scrolled": true 86 | }, 87 | "outputs": [ 88 | { 89 | "data": { 90 | "text/plain": [ 91 | "{'id': 1, 'name': 'django', 'slug': 'django'}" 92 | ] 93 | }, 94 | "execution_count": 5, 95 | "metadata": {}, 96 | "output_type": "execute_result" 97 | } 98 | ], 99 | "source": [ 100 | "s_tag.data" 101 | ] 102 | }, 103 | { 104 | "cell_type": "code", 105 | "execution_count": 6, 106 | "metadata": {}, 107 | "outputs": [ 108 | { 109 | "data": { 110 | "text/plain": [ 111 | "b'{\"id\":1,\"name\":\"django\",\"slug\":\"django\"}'" 112 | ] 113 | }, 114 | "execution_count": 6, 115 | "metadata": {}, 116 | "output_type": "execute_result" 117 | } 118 | ], 119 | "source": [ 120 | "JSONRenderer().render(s_tag.data)" 121 | ] 122 | }, 123 | { 124 | "cell_type": "code", 125 | "execution_count": 7, 126 | "metadata": { 127 | "scrolled": true 128 | }, 129 | "outputs": [], 130 | "source": [ 131 | "def render_json(serialized_object):\n", 132 | " \"\"\"Shortcut to make this notebook easier to read\"\"\"\n", 133 | " print(\n", 134 | " JSONRenderer().render(\n", 135 | " serialized_object.data,\n", 136 | " accepted_media_type='application/json; indent=4',\n", 137 | " ).decode('utf8')\n", 138 | " )" 139 | ] 140 | }, 141 | { 142 | "cell_type": "code", 143 | "execution_count": 8, 144 | "metadata": {}, 145 | "outputs": [ 146 | { 147 | "name": "stdout", 148 | "output_type": "stream", 149 | "text": [ 150 | "{\n", 151 | " \"id\": 1,\n", 152 | " \"name\": \"django\",\n", 153 | " \"slug\": \"django\"\n", 154 | "}\n" 155 | ] 156 | } 157 | ], 158 | "source": [ 159 | "render_json(s_tag)" 160 | ] 161 | }, 162 | { 163 | "cell_type": "code", 164 | "execution_count": 9, 165 | "metadata": {}, 166 | "outputs": [ 167 | { 168 | "data": { 169 | "text/plain": [ 170 | "'\\n1djangodjango'" 171 | ] 172 | }, 173 | "execution_count": 9, 174 | "metadata": {}, 175 | "output_type": "execute_result" 176 | } 177 | ], 178 | "source": [ 179 | "XMLRenderer().render(s_tag.data)" 180 | ] 181 | }, 182 | { 183 | "cell_type": "code", 184 | "execution_count": 10, 185 | "metadata": {}, 186 | "outputs": [], 187 | "source": [ 188 | "def render_xml(serialized_object):\n", 189 | " \"\"\"Shortcut to make this notebook easier to read\n", 190 | " \n", 191 | " If you need serious XML handling, compare \n", 192 | " LXML to Python's std-lib XML capabilities.\n", 193 | " \"\"\"\n", 194 | " print(\n", 195 | " xml_parse( # python std-lib\n", 196 | " XMLRenderer().render( # Django Rest Framework\n", 197 | " serialized_object.data\n", 198 | " ) \n", 199 | " ).toprettyxml()\n", 200 | " )" 201 | ] 202 | }, 203 | { 204 | "cell_type": "code", 205 | "execution_count": 11, 206 | "metadata": {}, 207 | "outputs": [ 208 | { 209 | "name": "stdout", 210 | "output_type": "stream", 211 | "text": [ 212 | "\n", 213 | "\n", 214 | "\t1\n", 215 | "\tdjango\n", 216 | "\tdjango\n", 217 | "\n", 218 | "\n" 219 | ] 220 | } 221 | ], 222 | "source": [ 223 | "render_xml(s_tag)" 224 | ] 225 | }, 226 | { 227 | "cell_type": "markdown", 228 | "metadata": {}, 229 | "source": [ 230 | "## Serialize Object with Relationships" 231 | ] 232 | }, 233 | { 234 | "cell_type": "code", 235 | "execution_count": 12, 236 | "metadata": {}, 237 | "outputs": [], 238 | "source": [ 239 | "jambon = Startup.objects.create(\n", 240 | " name='JamBon Software',\n", 241 | " slug='jambon-software',\n", 242 | " description='Software Consulting & Training for Web and Mobile Products',\n", 243 | " founded_date=date(2013, 1, 18),\n", 244 | " contact='django@jambonsw.com',\n", 245 | " website='https://www.jambonsw.com',\n", 246 | ")" 247 | ] 248 | }, 249 | { 250 | "cell_type": "code", 251 | "execution_count": 13, 252 | "metadata": {}, 253 | "outputs": [], 254 | "source": [ 255 | "jambon.tags.add(new_tag, Tag.objects.create(name='web'))" 256 | ] 257 | }, 258 | { 259 | "cell_type": "code", 260 | "execution_count": 14, 261 | "metadata": {}, 262 | "outputs": [], 263 | "source": [ 264 | "s_jambon = StartupSerializer(jambon)" 265 | ] 266 | }, 267 | { 268 | "cell_type": "code", 269 | "execution_count": 15, 270 | "metadata": {}, 271 | "outputs": [ 272 | { 273 | "data": { 274 | "text/plain": [ 275 | "StartupSerializer():\n", 276 | " id = IntegerField(read_only=True)\n", 277 | " name = CharField(max_length=31)\n", 278 | " slug = SlugField(allow_blank=True, max_length=31)\n", 279 | " description = CharField()\n", 280 | " founded_date = DateField()\n", 281 | " contact = EmailField()\n", 282 | " website = URLField(max_length=255)\n", 283 | " tags = TagSerializer(many=True):\n", 284 | " id = IntegerField(read_only=True)\n", 285 | " name = CharField(max_length=31)\n", 286 | " slug = SlugField(allow_blank=True, max_length=31)" 287 | ] 288 | }, 289 | "execution_count": 15, 290 | "metadata": {}, 291 | "output_type": "execute_result" 292 | } 293 | ], 294 | "source": [ 295 | "s_jambon" 296 | ] 297 | }, 298 | { 299 | "cell_type": "code", 300 | "execution_count": 16, 301 | "metadata": {}, 302 | "outputs": [ 303 | { 304 | "name": "stdout", 305 | "output_type": "stream", 306 | "text": [ 307 | "{'contact': 'django@jambonsw.com',\n", 308 | " 'description': 'Software Consulting & Training for Web and Mobile Products',\n", 309 | " 'founded_date': '2013-01-18',\n", 310 | " 'id': 1,\n", 311 | " 'name': 'JamBon Software',\n", 312 | " 'slug': 'jambon-software',\n", 313 | " 'tags': [OrderedDict([('id', 1), ('name', 'django'), ('slug', 'django')]),\n", 314 | " OrderedDict([('id', 2), ('name', 'web'), ('slug', 'web')])],\n", 315 | " 'website': 'https://www.jambonsw.com'}\n" 316 | ] 317 | } 318 | ], 319 | "source": [ 320 | "pprint(s_jambon.data)" 321 | ] 322 | }, 323 | { 324 | "cell_type": "code", 325 | "execution_count": 17, 326 | "metadata": {}, 327 | "outputs": [ 328 | { 329 | "name": "stdout", 330 | "output_type": "stream", 331 | "text": [ 332 | "{\n", 333 | " \"id\": 1,\n", 334 | " \"name\": \"JamBon Software\",\n", 335 | " \"slug\": \"jambon-software\",\n", 336 | " \"description\": \"Software Consulting & Training for Web and Mobile Products\",\n", 337 | " \"founded_date\": \"2013-01-18\",\n", 338 | " \"contact\": \"django@jambonsw.com\",\n", 339 | " \"website\": \"https://www.jambonsw.com\",\n", 340 | " \"tags\": [\n", 341 | " {\n", 342 | " \"id\": 1,\n", 343 | " \"name\": \"django\",\n", 344 | " \"slug\": \"django\"\n", 345 | " },\n", 346 | " {\n", 347 | " \"id\": 2,\n", 348 | " \"name\": \"web\",\n", 349 | " \"slug\": \"web\"\n", 350 | " }\n", 351 | " ]\n", 352 | "}\n" 353 | ] 354 | } 355 | ], 356 | "source": [ 357 | "render_json(s_jambon)" 358 | ] 359 | }, 360 | { 361 | "cell_type": "code", 362 | "execution_count": 18, 363 | "metadata": {}, 364 | "outputs": [ 365 | { 366 | "name": "stdout", 367 | "output_type": "stream", 368 | "text": [ 369 | "\n", 370 | "\n", 371 | "\t1\n", 372 | "\tJamBon Software\n", 373 | "\tjambon-software\n", 374 | "\tSoftware Consulting & Training for Web and Mobile Products\n", 375 | "\t2013-01-18\n", 376 | "\tdjango@jambonsw.com\n", 377 | "\thttps://www.jambonsw.com\n", 378 | "\t\n", 379 | "\t\t\n", 380 | "\t\t\t1\n", 381 | "\t\t\tdjango\n", 382 | "\t\t\tdjango\n", 383 | "\t\t\n", 384 | "\t\t\n", 385 | "\t\t\t2\n", 386 | "\t\t\tweb\n", 387 | "\t\t\tweb\n", 388 | "\t\t\n", 389 | "\t\n", 390 | "\n", 391 | "\n" 392 | ] 393 | } 394 | ], 395 | "source": [ 396 | "render_xml(s_jambon)" 397 | ] 398 | }, 399 | { 400 | "cell_type": "code", 401 | "execution_count": 19, 402 | "metadata": {}, 403 | "outputs": [], 404 | "source": [ 405 | "nl = NewsLink(\n", 406 | " title='JamBon Software rated best ever',\n", 407 | " slug='jambon-best',\n", 408 | " pub_date=date(2018,4,1),\n", 409 | " link='https://www.xkcd.com/353/',\n", 410 | " startup=jambon,\n", 411 | ")" 412 | ] 413 | }, 414 | { 415 | "cell_type": "code", 416 | "execution_count": 20, 417 | "metadata": {}, 418 | "outputs": [], 419 | "source": [ 420 | "s_nl = NewsLinkSerializer(nl)" 421 | ] 422 | }, 423 | { 424 | "cell_type": "code", 425 | "execution_count": 21, 426 | "metadata": {}, 427 | "outputs": [ 428 | { 429 | "data": { 430 | "text/plain": [ 431 | "NewsLinkSerializer():\n", 432 | " id = IntegerField(read_only=True)\n", 433 | " title = CharField(max_length=63)\n", 434 | " slug = SlugField(max_length=63)\n", 435 | " pub_date = DateField()\n", 436 | " link = URLField(max_length=255)\n", 437 | " startup = StartupSerializer():\n", 438 | " id = IntegerField(read_only=True)\n", 439 | " name = CharField(max_length=31)\n", 440 | " slug = SlugField(allow_blank=True, max_length=31)\n", 441 | " description = CharField()\n", 442 | " founded_date = DateField()\n", 443 | " contact = EmailField()\n", 444 | " website = URLField(max_length=255)\n", 445 | " tags = TagSerializer(many=True):\n", 446 | " id = IntegerField(read_only=True)\n", 447 | " name = CharField(max_length=31)\n", 448 | " slug = SlugField(allow_blank=True, max_length=31)" 449 | ] 450 | }, 451 | "execution_count": 21, 452 | "metadata": {}, 453 | "output_type": "execute_result" 454 | } 455 | ], 456 | "source": [ 457 | "s_nl" 458 | ] 459 | }, 460 | { 461 | "cell_type": "code", 462 | "execution_count": 22, 463 | "metadata": {}, 464 | "outputs": [ 465 | { 466 | "name": "stdout", 467 | "output_type": "stream", 468 | "text": [ 469 | "{'id': None,\n", 470 | " 'link': 'https://www.xkcd.com/353/',\n", 471 | " 'pub_date': '2018-04-01',\n", 472 | " 'slug': 'jambon-best',\n", 473 | " 'startup': OrderedDict([('id', 1),\n", 474 | " ('name', 'JamBon Software'),\n", 475 | " ('slug', 'jambon-software'),\n", 476 | " ('description',\n", 477 | " 'Software Consulting & Training for Web and Mobile '\n", 478 | " 'Products'),\n", 479 | " ('founded_date', '2013-01-18'),\n", 480 | " ('contact', 'django@jambonsw.com'),\n", 481 | " ('website', 'https://www.jambonsw.com'),\n", 482 | " ('tags',\n", 483 | " [OrderedDict([('id', 1),\n", 484 | " ('name', 'django'),\n", 485 | " ('slug', 'django')]),\n", 486 | " OrderedDict([('id', 2),\n", 487 | " ('name', 'web'),\n", 488 | " ('slug', 'web')])])]),\n", 489 | " 'title': 'JamBon Software rated best ever'}\n" 490 | ] 491 | } 492 | ], 493 | "source": [ 494 | "pprint(s_nl.data)" 495 | ] 496 | }, 497 | { 498 | "cell_type": "code", 499 | "execution_count": 23, 500 | "metadata": {}, 501 | "outputs": [ 502 | { 503 | "name": "stdout", 504 | "output_type": "stream", 505 | "text": [ 506 | "{\n", 507 | " \"id\": null,\n", 508 | " \"title\": \"JamBon Software rated best ever\",\n", 509 | " \"slug\": \"jambon-best\",\n", 510 | " \"pub_date\": \"2018-04-01\",\n", 511 | " \"link\": \"https://www.xkcd.com/353/\",\n", 512 | " \"startup\": {\n", 513 | " \"id\": 1,\n", 514 | " \"name\": \"JamBon Software\",\n", 515 | " \"slug\": \"jambon-software\",\n", 516 | " \"description\": \"Software Consulting & Training for Web and Mobile Products\",\n", 517 | " \"founded_date\": \"2013-01-18\",\n", 518 | " \"contact\": \"django@jambonsw.com\",\n", 519 | " \"website\": \"https://www.jambonsw.com\",\n", 520 | " \"tags\": [\n", 521 | " {\n", 522 | " \"id\": 1,\n", 523 | " \"name\": \"django\",\n", 524 | " \"slug\": \"django\"\n", 525 | " },\n", 526 | " {\n", 527 | " \"id\": 2,\n", 528 | " \"name\": \"web\",\n", 529 | " \"slug\": \"web\"\n", 530 | " }\n", 531 | " ]\n", 532 | " }\n", 533 | "}\n" 534 | ] 535 | } 536 | ], 537 | "source": [ 538 | "render_json(s_nl)" 539 | ] 540 | }, 541 | { 542 | "cell_type": "code", 543 | "execution_count": 24, 544 | "metadata": {}, 545 | "outputs": [ 546 | { 547 | "name": "stdout", 548 | "output_type": "stream", 549 | "text": [ 550 | "\n", 551 | "\n", 552 | "\t\n", 553 | "\tJamBon Software rated best ever\n", 554 | "\tjambon-best\n", 555 | "\t2018-04-01\n", 556 | "\thttps://www.xkcd.com/353/\n", 557 | "\t\n", 558 | "\t\t1\n", 559 | "\t\tJamBon Software\n", 560 | "\t\tjambon-software\n", 561 | "\t\tSoftware Consulting & Training for Web and Mobile Products\n", 562 | "\t\t2013-01-18\n", 563 | "\t\tdjango@jambonsw.com\n", 564 | "\t\thttps://www.jambonsw.com\n", 565 | "\t\t\n", 566 | "\t\t\t\n", 567 | "\t\t\t\t1\n", 568 | "\t\t\t\tdjango\n", 569 | "\t\t\t\tdjango\n", 570 | "\t\t\t\n", 571 | "\t\t\t\n", 572 | "\t\t\t\t2\n", 573 | "\t\t\t\tweb\n", 574 | "\t\t\t\tweb\n", 575 | "\t\t\t\n", 576 | "\t\t\n", 577 | "\t\n", 578 | "\n", 579 | "\n" 580 | ] 581 | } 582 | ], 583 | "source": [ 584 | "render_xml(s_nl)" 585 | ] 586 | } 587 | ], 588 | "metadata": { 589 | "kernelspec": { 590 | "display_name": "Django Shell-Plus", 591 | "language": "python", 592 | "name": "django_extensions" 593 | }, 594 | "language_info": { 595 | "codemirror_mode": { 596 | "name": "ipython", 597 | "version": 3 598 | }, 599 | "file_extension": ".py", 600 | "mimetype": "text/x-python", 601 | "name": "python", 602 | "nbconvert_exporter": "python", 603 | "pygments_lexer": "ipython3", 604 | "version": "3.7.0" 605 | } 606 | }, 607 | "nbformat": 4, 608 | "nbformat_minor": 2 609 | } 610 | -------------------------------------------------------------------------------- /src/Lesson_04.04_render_templates.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Template Objects in Python" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "## Introducting Template and Context via Direct Usage" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": 1, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "from django.template import Context, Template" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": 2, 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "template = Template(\"{{ superhero }} is the very best superhero.\")" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": 3, 38 | "metadata": {}, 39 | "outputs": [ 40 | { 41 | "data": { 42 | "text/plain": [ 43 | "django.template.base.Template" 44 | ] 45 | }, 46 | "execution_count": 3, 47 | "metadata": {}, 48 | "output_type": "execute_result" 49 | } 50 | ], 51 | "source": [ 52 | "type(template)" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": 4, 58 | "metadata": {}, 59 | "outputs": [ 60 | { 61 | "data": { 62 | "text/plain": [ 63 | "'batman is the very best superhero.'" 64 | ] 65 | }, 66 | "execution_count": 4, 67 | "metadata": {}, 68 | "output_type": "execute_result" 69 | } 70 | ], 71 | "source": [ 72 | "context = Context({\"superhero\": \"batman\"})\n", 73 | "template.render(context)" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": 5, 79 | "metadata": {}, 80 | "outputs": [ 81 | { 82 | "data": { 83 | "text/plain": [ 84 | "'Wonder Woman is the very best superhero.'" 85 | ] 86 | }, 87 | "execution_count": 5, 88 | "metadata": {}, 89 | "output_type": "execute_result" 90 | } 91 | ], 92 | "source": [ 93 | "# similar to f-strings in Python 3.6\n", 94 | "superhero = \"wonder woman\"\n", 95 | "# note the title method\n", 96 | "f\"{superhero.title()} is the very best superhero.\"" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": 6, 102 | "metadata": {}, 103 | "outputs": [], 104 | "source": [ 105 | "# note the title filter\n", 106 | "template = Template(\"{{ superhero|title }} is the very best superhero.\")" 107 | ] 108 | }, 109 | { 110 | "cell_type": "markdown", 111 | "metadata": {}, 112 | "source": [ 113 | "See [Django's documentation](https://docs.djangoproject.com/en/2.1/ref/templates/builtins/) for a full list of all the template filters available." 114 | ] 115 | }, 116 | { 117 | "cell_type": "code", 118 | "execution_count": 7, 119 | "metadata": {}, 120 | "outputs": [ 121 | { 122 | "data": { 123 | "text/plain": [ 124 | "'Batman is the very best superhero.'" 125 | ] 126 | }, 127 | "execution_count": 7, 128 | "metadata": {}, 129 | "output_type": "execute_result" 130 | } 131 | ], 132 | "source": [ 133 | "template.render(context)" 134 | ] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "execution_count": 8, 139 | "metadata": {}, 140 | "outputs": [ 141 | { 142 | "data": { 143 | "text/plain": [ 144 | "'Superman is the very best superhero.'" 145 | ] 146 | }, 147 | "execution_count": 8, 148 | "metadata": {}, 149 | "output_type": "execute_result" 150 | } 151 | ], 152 | "source": [ 153 | "# reusable!\n", 154 | "template.render(Context({\"superhero\": \"superman\"}))" 155 | ] 156 | }, 157 | { 158 | "cell_type": "code", 159 | "execution_count": 9, 160 | "metadata": {}, 161 | "outputs": [ 162 | { 163 | "data": { 164 | "text/plain": [ 165 | "' is the very best superhero.'" 166 | ] 167 | }, 168 | "execution_count": 9, 169 | "metadata": {}, 170 | "output_type": "execute_result" 171 | } 172 | ], 173 | "source": [ 174 | "# however...\n", 175 | "template.render(Context())" 176 | ] 177 | }, 178 | { 179 | "cell_type": "code", 180 | "execution_count": 10, 181 | "metadata": {}, 182 | "outputs": [ 183 | { 184 | "name": "stdout", 185 | "output_type": "stream", 186 | "text": [ 187 | "Ouch!\n", 188 | "she said dutifully\n", 189 | "as she jumped into her convertible boat\n", 190 | "and drove off with her pineapple.\n", 191 | "\n" 192 | ] 193 | } 194 | ], 195 | "source": [ 196 | "# allows for keys and attributes\n", 197 | "template = Template(\n", 198 | " \"{{ ml.exclaim }}!\\n\"\n", 199 | " \"she said {{ ml.adverb }}\\n\"\n", 200 | " \"as she jumped into her convertible {{ ml.noun1 }}\\n\"\n", 201 | " \"and drove off with her {{ ml.noun2 }}.\\n\"\n", 202 | ")\n", 203 | "mad_lib = {\n", 204 | " \"exclaim\": \"Ouch\",\n", 205 | " \"adverb\": \"dutifully\",\n", 206 | " \"noun1\": \"boat\",\n", 207 | " \"noun2\": \"pineapple\",\n", 208 | "}\n", 209 | "context = Context({\"ml\": mad_lib})\n", 210 | "print(template.render(context))" 211 | ] 212 | }, 213 | { 214 | "cell_type": "markdown", 215 | "metadata": {}, 216 | "source": [ 217 | "## Loading Templates from Disk" 218 | ] 219 | }, 220 | { 221 | "cell_type": "code", 222 | "execution_count": 11, 223 | "metadata": {}, 224 | "outputs": [], 225 | "source": [ 226 | "from django.template import loader" 227 | ] 228 | }, 229 | { 230 | "cell_type": "code", 231 | "execution_count": 12, 232 | "metadata": {}, 233 | "outputs": [], 234 | "source": [ 235 | "template = loader.get_template('tag/list.html')" 236 | ] 237 | }, 238 | { 239 | "cell_type": "code", 240 | "execution_count": 13, 241 | "metadata": {}, 242 | "outputs": [ 243 | { 244 | "data": { 245 | "text/plain": [ 246 | "django.template.backends.django.Template" 247 | ] 248 | }, 249 | "execution_count": 13, 250 | "metadata": {}, 251 | "output_type": "execute_result" 252 | } 253 | ], 254 | "source": [ 255 | "type(template) # slightly different type!\n", 256 | "# templates is section before were django.template.base.Template" 257 | ] 258 | }, 259 | { 260 | "cell_type": "code", 261 | "execution_count": 14, 262 | "metadata": {}, 263 | "outputs": [ 264 | { 265 | "name": "stdout", 266 | "output_type": "stream", 267 | "text": [ 268 | "\n", 269 | "\n", 270 | " \n", 271 | " \n", 272 | " \n", 273 | " \n", 274 | " Startup Organizer - Tag List\n", 275 | "\n", 276 | " \n", 277 | " \n", 278 | " \n", 279 | " \n", 286 | " \n", 287 | "

Tag List

\n", 288 | " \n", 306 | "\n", 307 | " \n", 308 | "\n", 309 | "\n" 310 | ] 311 | } 312 | ], 313 | "source": [ 314 | "best_list = [\n", 315 | " {'name': 'Pirates'},\n", 316 | " {'name': 'Ninjas'},\n", 317 | " {'name': 'Cowboys'},\n", 318 | "]\n", 319 | "context = {'tag_list': best_list} # a plain Python dict!\n", 320 | "# as of Django 1.10, passing a Context instance is not longer supported\n", 321 | "print(template.render(context))" 322 | ] 323 | } 324 | ], 325 | "metadata": { 326 | "kernelspec": { 327 | "display_name": "Django Shell-Plus", 328 | "language": "python", 329 | "name": "django_extensions" 330 | }, 331 | "language_info": { 332 | "codemirror_mode": { 333 | "name": "ipython", 334 | "version": 3 335 | }, 336 | "file_extension": ".py", 337 | "mimetype": "text/x-python", 338 | "name": "python", 339 | "nbconvert_exporter": "python", 340 | "pygments_lexer": "ipython3", 341 | "version": "3.7.0" 342 | } 343 | }, 344 | "nbformat": 4, 345 | "nbformat_minor": 2 346 | } 347 | -------------------------------------------------------------------------------- /src/Lesson_04.06_create_urls_by_reversing_url_paths.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Create URLs by Reversing URL paths" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "from django.urls import NoReverseMatch, reverse" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": 2, 22 | "metadata": {}, 23 | "outputs": [ 24 | { 25 | "data": { 26 | "text/plain": [ 27 | "'/tag/'" 28 | ] 29 | }, 30 | "execution_count": 2, 31 | "metadata": {}, 32 | "output_type": "execute_result" 33 | } 34 | ], 35 | "source": [ 36 | "reverse(\"tag_list\") # name of our Tag List URL path" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": 3, 42 | "metadata": {}, 43 | "outputs": [ 44 | { 45 | "name": "stdout", 46 | "output_type": "stream", 47 | "text": [ 48 | "Reverse for 'tag_detail' with no arguments not found. 1 pattern(s) tried: ['tag/(?P[^/]+)/$']\n" 49 | ] 50 | } 51 | ], 52 | "source": [ 53 | "# what about Tag Detail?\n", 54 | "try:\n", 55 | " reverse(\"tag_detail\")\n", 56 | "except NoReverseMatch as err:\n", 57 | " print(err)" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": 4, 63 | "metadata": {}, 64 | "outputs": [ 65 | { 66 | "data": { 67 | "text/plain": [ 68 | "'/tag/slugilicious/'" 69 | ] 70 | }, 71 | "execution_count": 4, 72 | "metadata": {}, 73 | "output_type": "execute_result" 74 | } 75 | ], 76 | "source": [ 77 | "# we must include the Tag's slug!\n", 78 | "# (or whatever else is needed in the URL)\n", 79 | "reverse(\n", 80 | " \"tag_detail\",\n", 81 | " kwargs={\n", 82 | " \"slug\": \"slugilicious\"\n", 83 | " }\n", 84 | ")" 85 | ] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "execution_count": 5, 90 | "metadata": {}, 91 | "outputs": [ 92 | { 93 | "data": { 94 | "text/plain": [ 95 | "'/blog/2018/8/now-recording/'" 96 | ] 97 | }, 98 | "execution_count": 5, 99 | "metadata": {}, 100 | "output_type": "execute_result" 101 | } 102 | ], 103 | "source": [ 104 | "reverse(\n", 105 | " \"post_detail\",\n", 106 | " kwargs={\n", 107 | " \"year\": 2018,\n", 108 | " \"month\": 8,\n", 109 | " \"slug\": \"now-recording\"\n", 110 | " }\n", 111 | ")" 112 | ] 113 | }, 114 | { 115 | "cell_type": "markdown", 116 | "metadata": {}, 117 | "source": [ 118 | "## Reverse in Templates" 119 | ] 120 | }, 121 | { 122 | "cell_type": "code", 123 | "execution_count": 6, 124 | "metadata": {}, 125 | "outputs": [], 126 | "source": [ 127 | "from django.template import Context, Template" 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": 7, 133 | "metadata": {}, 134 | "outputs": [ 135 | { 136 | "data": { 137 | "text/plain": [ 138 | "'/tag/'" 139 | ] 140 | }, 141 | "execution_count": 7, 142 | "metadata": {}, 143 | "output_type": "execute_result" 144 | } 145 | ], 146 | "source": [ 147 | "code = \"{% url 'tag_list' %}\"\n", 148 | "template = Template(code)\n", 149 | "template.render(Context())" 150 | ] 151 | }, 152 | { 153 | "cell_type": "code", 154 | "execution_count": 8, 155 | "metadata": {}, 156 | "outputs": [ 157 | { 158 | "data": { 159 | "text/plain": [ 160 | "'/tag/slugerific/'" 161 | ] 162 | }, 163 | "execution_count": 8, 164 | "metadata": {}, 165 | "output_type": "execute_result" 166 | } 167 | ], 168 | "source": [ 169 | "code = \"{% url 'tag_detail' slug='slugerific' %}\"\n", 170 | "template = Template(code)\n", 171 | "template.render(Context())" 172 | ] 173 | }, 174 | { 175 | "cell_type": "code", 176 | "execution_count": 9, 177 | "metadata": { 178 | "scrolled": true 179 | }, 180 | "outputs": [ 181 | { 182 | "data": { 183 | "text/plain": [ 184 | "'/blog/2018/8/keyboards-are-my-friends/'" 185 | ] 186 | }, 187 | "execution_count": 9, 188 | "metadata": {}, 189 | "output_type": "execute_result" 190 | } 191 | ], 192 | "source": [ 193 | "# the problem is that this gets really long:\n", 194 | "code = \"{% url 'post_detail' year=2018 month=8 slug='keyboards-are-my-friends' %}\"\n", 195 | "template = Template(code)\n", 196 | "template.render(Context())" 197 | ] 198 | }, 199 | { 200 | "cell_type": "markdown", 201 | "metadata": {}, 202 | "source": [ 203 | "Django instead [recommends](https://docs.djangoproject.com/en/2.1/ref/models/instances/#get-absolute-url) creating a method on models to direct to the canonical URL. Let's do just that!" 204 | ] 205 | } 206 | ], 207 | "metadata": { 208 | "kernelspec": { 209 | "display_name": "Django Shell-Plus", 210 | "language": "python", 211 | "name": "django_extensions" 212 | }, 213 | "language_info": { 214 | "codemirror_mode": { 215 | "name": "ipython", 216 | "version": 3 217 | }, 218 | "file_extension": ".py", 219 | "mimetype": "text/x-python", 220 | "name": "python", 221 | "nbconvert_exporter": "python", 222 | "pygments_lexer": "ipython3", 223 | "version": "3.7.0" 224 | } 225 | }, 226 | "nbformat": 4, 227 | "nbformat_minor": 2 228 | } 229 | -------------------------------------------------------------------------------- /src/Lesson_05.01_save_serialized_data_to_database.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Save Serializer Data to the Database" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "## Basic Usage" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": 1, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "from datetime import date\n", 24 | "\n", 25 | "from django.test import RequestFactory\n", 26 | "\n", 27 | "from organizer.models import Tag\n", 28 | "from organizer.serializers import StartupSerializer, TagSerializer" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": 2, 34 | "metadata": {}, 35 | "outputs": [], 36 | "source": [ 37 | "fake_request = RequestFactory().get(\"/api/v1/tag/\")" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 3, 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "data = {\"name\": \"new tag\"}\n", 47 | "s_tag = TagSerializer(\n", 48 | " data=data,\n", 49 | " # request is necessary for full URL reversal\n", 50 | " # needed by url field of HyperlinkedModelSerializer\n", 51 | " context={\"request\": fake_request},\n", 52 | ")" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": 4, 58 | "metadata": {}, 59 | "outputs": [ 60 | { 61 | "data": { 62 | "text/plain": [ 63 | "True" 64 | ] 65 | }, 66 | "execution_count": 4, 67 | "metadata": {}, 68 | "output_type": "execute_result" 69 | } 70 | ], 71 | "source": [ 72 | "s_tag.is_valid()" 73 | ] 74 | }, 75 | { 76 | "cell_type": "code", 77 | "execution_count": 5, 78 | "metadata": {}, 79 | "outputs": [ 80 | { 81 | "data": { 82 | "text/plain": [ 83 | "{}" 84 | ] 85 | }, 86 | "execution_count": 5, 87 | "metadata": {}, 88 | "output_type": "execute_result" 89 | } 90 | ], 91 | "source": [ 92 | "s_tag.errors" 93 | ] 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": 6, 98 | "metadata": {}, 99 | "outputs": [ 100 | { 101 | "data": { 102 | "text/plain": [ 103 | "False" 104 | ] 105 | }, 106 | "execution_count": 6, 107 | "metadata": {}, 108 | "output_type": "execute_result" 109 | } 110 | ], 111 | "source": [ 112 | "Tag.objects.filter(name=\"new tag\").exists()" 113 | ] 114 | }, 115 | { 116 | "cell_type": "code", 117 | "execution_count": 7, 118 | "metadata": {}, 119 | "outputs": [ 120 | { 121 | "data": { 122 | "text/plain": [ 123 | "" 124 | ] 125 | }, 126 | "execution_count": 7, 127 | "metadata": {}, 128 | "output_type": "execute_result" 129 | } 130 | ], 131 | "source": [ 132 | "s_tag.save()" 133 | ] 134 | }, 135 | { 136 | "cell_type": "code", 137 | "execution_count": 8, 138 | "metadata": {}, 139 | "outputs": [ 140 | { 141 | "data": { 142 | "text/plain": [ 143 | "True" 144 | ] 145 | }, 146 | "execution_count": 8, 147 | "metadata": {}, 148 | "output_type": "execute_result" 149 | } 150 | ], 151 | "source": [ 152 | "Tag.objects.filter(name=\"new tag\").exists()" 153 | ] 154 | }, 155 | { 156 | "cell_type": "markdown", 157 | "metadata": {}, 158 | "source": [ 159 | "## Errors and Extra Values" 160 | ] 161 | }, 162 | { 163 | "cell_type": "code", 164 | "execution_count": 9, 165 | "metadata": {}, 166 | "outputs": [], 167 | "source": [ 168 | "s_tag = TagSerializer(\n", 169 | " data={},\n", 170 | " context={\"request\": fake_request},\n", 171 | ")" 172 | ] 173 | }, 174 | { 175 | "cell_type": "code", 176 | "execution_count": 10, 177 | "metadata": {}, 178 | "outputs": [ 179 | { 180 | "data": { 181 | "text/plain": [ 182 | "False" 183 | ] 184 | }, 185 | "execution_count": 10, 186 | "metadata": {}, 187 | "output_type": "execute_result" 188 | } 189 | ], 190 | "source": [ 191 | "s_tag.is_valid()" 192 | ] 193 | }, 194 | { 195 | "cell_type": "code", 196 | "execution_count": 11, 197 | "metadata": {}, 198 | "outputs": [ 199 | { 200 | "data": { 201 | "text/plain": [ 202 | "{'name': [ErrorDetail(string='This field is required.', code='required')]}" 203 | ] 204 | }, 205 | "execution_count": 11, 206 | "metadata": {}, 207 | "output_type": "execute_result" 208 | } 209 | ], 210 | "source": [ 211 | "s_tag.errors" 212 | ] 213 | }, 214 | { 215 | "cell_type": "code", 216 | "execution_count": 12, 217 | "metadata": {}, 218 | "outputs": [], 219 | "source": [ 220 | "s_tag = TagSerializer(\n", 221 | " data={\n", 222 | " \"name\": 'newer tag',\n", 223 | " \"ignored\": \"this value is ignored\"\n", 224 | " },\n", 225 | " context={\"request\": fake_request},\n", 226 | ")" 227 | ] 228 | }, 229 | { 230 | "cell_type": "code", 231 | "execution_count": 13, 232 | "metadata": {}, 233 | "outputs": [ 234 | { 235 | "data": { 236 | "text/plain": [ 237 | "True" 238 | ] 239 | }, 240 | "execution_count": 13, 241 | "metadata": {}, 242 | "output_type": "execute_result" 243 | } 244 | ], 245 | "source": [ 246 | "s_tag.is_valid()" 247 | ] 248 | }, 249 | { 250 | "cell_type": "code", 251 | "execution_count": 14, 252 | "metadata": {}, 253 | "outputs": [ 254 | { 255 | "data": { 256 | "text/plain": [ 257 | "{}" 258 | ] 259 | }, 260 | "execution_count": 14, 261 | "metadata": {}, 262 | "output_type": "execute_result" 263 | } 264 | ], 265 | "source": [ 266 | "s_tag.errors" 267 | ] 268 | }, 269 | { 270 | "cell_type": "code", 271 | "execution_count": 15, 272 | "metadata": {}, 273 | "outputs": [ 274 | { 275 | "data": { 276 | "text/plain": [ 277 | "" 278 | ] 279 | }, 280 | "execution_count": 15, 281 | "metadata": {}, 282 | "output_type": "execute_result" 283 | } 284 | ], 285 | "source": [ 286 | "s_tag.save()" 287 | ] 288 | }, 289 | { 290 | "cell_type": "markdown", 291 | "metadata": {}, 292 | "source": [ 293 | "## Nested Serializer Handling" 294 | ] 295 | }, 296 | { 297 | "cell_type": "code", 298 | "execution_count": 16, 299 | "metadata": {}, 300 | "outputs": [], 301 | "source": [ 302 | "s_startup = StartupSerializer(\n", 303 | " data=dict(\n", 304 | " name=\"JamBon Software LLC\",\n", 305 | " slug=\"jambon-software-llc\",\n", 306 | " description=\"Consulting & training for web and mobile products.\",\n", 307 | " founded_date=date(2013, 1, 13),\n", 308 | " contact=\"django@jambonsw.com\", # not a real email\n", 309 | " website=\"https://jambonsw.com\",\n", 310 | " tags=[\n", 311 | " {\"name\": \"newest tag\"},\n", 312 | " ],\n", 313 | " ),\n", 314 | " context={\"request\": RequestFactory().get(\"/api/v1/startup/\")},\n", 315 | ")" 316 | ] 317 | }, 318 | { 319 | "cell_type": "code", 320 | "execution_count": 17, 321 | "metadata": {}, 322 | "outputs": [ 323 | { 324 | "data": { 325 | "text/plain": [ 326 | "True" 327 | ] 328 | }, 329 | "execution_count": 17, 330 | "metadata": {}, 331 | "output_type": "execute_result" 332 | } 333 | ], 334 | "source": [ 335 | "s_startup.is_valid()" 336 | ] 337 | }, 338 | { 339 | "cell_type": "code", 340 | "execution_count": 18, 341 | "metadata": {}, 342 | "outputs": [ 343 | { 344 | "data": { 345 | "text/plain": [ 346 | "{}" 347 | ] 348 | }, 349 | "execution_count": 18, 350 | "metadata": {}, 351 | "output_type": "execute_result" 352 | } 353 | ], 354 | "source": [ 355 | "s_startup.errors" 356 | ] 357 | }, 358 | { 359 | "cell_type": "code", 360 | "execution_count": 19, 361 | "metadata": { 362 | "scrolled": true 363 | }, 364 | "outputs": [ 365 | { 366 | "name": "stdout", 367 | "output_type": "stream", 368 | "text": [ 369 | "The `.create()` method does not support writable nested fields by default.\n", 370 | "Write an explicit `.create()` method for serializer `organizer.serializers.StartupSerializer`, or set `read_only=True` on nested serializer fields.\n" 371 | ] 372 | } 373 | ], 374 | "source": [ 375 | "# serializers inside serializers is not supported\n", 376 | "# we will therefore need to modify all our serializers\n", 377 | "# that use other serializers for model relations:\n", 378 | "# StartupSerializer, NewsLinkSerializer, PostSerializer\n", 379 | "try:\n", 380 | " s_startup.save()\n", 381 | "except AssertionError as err:\n", 382 | " print(err)" 383 | ] 384 | }, 385 | { 386 | "cell_type": "markdown", 387 | "metadata": {}, 388 | "source": [ 389 | "## Notebook Cleanup" 390 | ] 391 | }, 392 | { 393 | "cell_type": "code", 394 | "execution_count": 20, 395 | "metadata": {}, 396 | "outputs": [ 397 | { 398 | "data": { 399 | "text/plain": [ 400 | "(2, {'blog.Post_tags': 0, 'organizer.Startup_tags': 0, 'organizer.Tag': 2})" 401 | ] 402 | }, 403 | "execution_count": 20, 404 | "metadata": {}, 405 | "output_type": "execute_result" 406 | } 407 | ], 408 | "source": [ 409 | "Tag.objects.filter(name__endswith=\" tag\").delete()" 410 | ] 411 | } 412 | ], 413 | "metadata": { 414 | "kernelspec": { 415 | "display_name": "Django Shell-Plus", 416 | "language": "python", 417 | "name": "django_extensions" 418 | }, 419 | "language_info": { 420 | "codemirror_mode": { 421 | "name": "ipython", 422 | "version": 3 423 | }, 424 | "file_extension": ".py", 425 | "mimetype": "text/x-python", 426 | "name": "python", 427 | "nbconvert_exporter": "python", 428 | "pygments_lexer": "ipython3", 429 | "version": "3.7.0" 430 | } 431 | }, 432 | "nbformat": 4, 433 | "nbformat_minor": 2 434 | } 435 | -------------------------------------------------------------------------------- /src/Lesson_05.02_update_database_via_serializer.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Update an existing Tag" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "from django.test import RequestFactory\n", 17 | "\n", 18 | "from organizer.models import Tag\n", 19 | "from organizer.serializers import StartupSerializer, TagSerializer" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": 2, 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [ 28 | "fake_request = RequestFactory().get(\"/api/v1/tag/\")" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": 3, 34 | "metadata": {}, 35 | "outputs": [], 36 | "source": [ 37 | "tag, created = Tag.objects.get_or_create(\n", 38 | " slug=\"django\",\n", 39 | " defaults={\n", 40 | " \"name\": \"django\",\n", 41 | " },\n", 42 | ")" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 4, 48 | "metadata": {}, 49 | "outputs": [], 50 | "source": [ 51 | "s_tag = TagSerializer(\n", 52 | " tag, # the existing object\n", 53 | " data={\n", 54 | " \"name\": \"django!\",\n", 55 | " },\n", 56 | " context={\"request\": fake_request},\n", 57 | ")" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": 5, 63 | "metadata": {}, 64 | "outputs": [ 65 | { 66 | "data": { 67 | "text/plain": [ 68 | "True" 69 | ] 70 | }, 71 | "execution_count": 5, 72 | "metadata": {}, 73 | "output_type": "execute_result" 74 | } 75 | ], 76 | "source": [ 77 | "s_tag.is_valid()" 78 | ] 79 | }, 80 | { 81 | "cell_type": "code", 82 | "execution_count": 6, 83 | "metadata": {}, 84 | "outputs": [ 85 | { 86 | "data": { 87 | "text/plain": [ 88 | "" 89 | ] 90 | }, 91 | "execution_count": 6, 92 | "metadata": {}, 93 | "output_type": "execute_result" 94 | } 95 | ], 96 | "source": [ 97 | "s_tag.save()" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": 7, 103 | "metadata": {}, 104 | "outputs": [], 105 | "source": [ 106 | "tag.refresh_from_db()" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": 8, 112 | "metadata": {}, 113 | "outputs": [ 114 | { 115 | "data": { 116 | "text/plain": [ 117 | "'django!'" 118 | ] 119 | }, 120 | "execution_count": 8, 121 | "metadata": {}, 122 | "output_type": "execute_result" 123 | } 124 | ], 125 | "source": [ 126 | "tag.name" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": 9, 132 | "metadata": {}, 133 | "outputs": [ 134 | { 135 | "data": { 136 | "text/plain": [ 137 | "'django'" 138 | ] 139 | }, 140 | "execution_count": 9, 141 | "metadata": {}, 142 | "output_type": "execute_result" 143 | } 144 | ], 145 | "source": [ 146 | "tag.slug" 147 | ] 148 | }, 149 | { 150 | "cell_type": "markdown", 151 | "metadata": {}, 152 | "source": [ 153 | "## Partial Updates and Ignored Fields" 154 | ] 155 | }, 156 | { 157 | "cell_type": "code", 158 | "execution_count": 10, 159 | "metadata": {}, 160 | "outputs": [], 161 | "source": [ 162 | "s_tag = TagSerializer(\n", 163 | " tag, # the existing object\n", 164 | " data={\n", 165 | " \"slug\": \"hendrix\",\n", 166 | " \"ignored\": \"this won't cause problems\"\n", 167 | " },\n", 168 | " context={\"request\": fake_request},\n", 169 | ")" 170 | ] 171 | }, 172 | { 173 | "cell_type": "code", 174 | "execution_count": 11, 175 | "metadata": {}, 176 | "outputs": [ 177 | { 178 | "data": { 179 | "text/plain": [ 180 | "False" 181 | ] 182 | }, 183 | "execution_count": 11, 184 | "metadata": {}, 185 | "output_type": "execute_result" 186 | } 187 | ], 188 | "source": [ 189 | "s_tag.is_valid()" 190 | ] 191 | }, 192 | { 193 | "cell_type": "code", 194 | "execution_count": 12, 195 | "metadata": {}, 196 | "outputs": [ 197 | { 198 | "data": { 199 | "text/plain": [ 200 | "{'name': [ErrorDetail(string='This field is required.', code='required')]}" 201 | ] 202 | }, 203 | "execution_count": 12, 204 | "metadata": {}, 205 | "output_type": "execute_result" 206 | } 207 | ], 208 | "source": [ 209 | "s_tag.errors" 210 | ] 211 | }, 212 | { 213 | "cell_type": "code", 214 | "execution_count": 13, 215 | "metadata": {}, 216 | "outputs": [], 217 | "source": [ 218 | "s_tag = TagSerializer(\n", 219 | " tag, # the existing object\n", 220 | " data={\n", 221 | " \"slug\": \"hendrix\",\n", 222 | " \"ignored\": \"this won't cause problems\"\n", 223 | " },\n", 224 | " partial=True,\n", 225 | " context={\"request\": fake_request},\n", 226 | ")" 227 | ] 228 | }, 229 | { 230 | "cell_type": "code", 231 | "execution_count": 14, 232 | "metadata": {}, 233 | "outputs": [ 234 | { 235 | "data": { 236 | "text/plain": [ 237 | "True" 238 | ] 239 | }, 240 | "execution_count": 14, 241 | "metadata": {}, 242 | "output_type": "execute_result" 243 | } 244 | ], 245 | "source": [ 246 | "s_tag.is_valid()" 247 | ] 248 | }, 249 | { 250 | "cell_type": "code", 251 | "execution_count": 15, 252 | "metadata": {}, 253 | "outputs": [ 254 | { 255 | "data": { 256 | "text/plain": [ 257 | "" 258 | ] 259 | }, 260 | "execution_count": 15, 261 | "metadata": {}, 262 | "output_type": "execute_result" 263 | } 264 | ], 265 | "source": [ 266 | "s_tag.save()" 267 | ] 268 | }, 269 | { 270 | "cell_type": "code", 271 | "execution_count": 16, 272 | "metadata": {}, 273 | "outputs": [], 274 | "source": [ 275 | "tag.refresh_from_db()" 276 | ] 277 | }, 278 | { 279 | "cell_type": "code", 280 | "execution_count": 17, 281 | "metadata": { 282 | "scrolled": true 283 | }, 284 | "outputs": [ 285 | { 286 | "data": { 287 | "text/plain": [ 288 | "'django'" 289 | ] 290 | }, 291 | "execution_count": 17, 292 | "metadata": {}, 293 | "output_type": "execute_result" 294 | } 295 | ], 296 | "source": [ 297 | "tag.slug" 298 | ] 299 | }, 300 | { 301 | "cell_type": "code", 302 | "execution_count": 18, 303 | "metadata": {}, 304 | "outputs": [ 305 | { 306 | "data": { 307 | "text/plain": [ 308 | "False" 309 | ] 310 | }, 311 | "execution_count": 18, 312 | "metadata": {}, 313 | "output_type": "execute_result" 314 | } 315 | ], 316 | "source": [ 317 | "# the AutoSlugField tells other parts of\n", 318 | "# Django and third-party apps to not update this field\n", 319 | "# and so the Serializer silently leaves it alone!\n", 320 | "Tag._meta.get_field(\"slug\").editable" 321 | ] 322 | }, 323 | { 324 | "cell_type": "code", 325 | "execution_count": 19, 326 | "metadata": {}, 327 | "outputs": [ 328 | { 329 | "data": { 330 | "text/plain": [ 331 | "TagSerializer(, context={'request': }, data={'slug': 'hendrix', 'ignored': \"this won't cause problems\"}, partial=True):\n", 332 | " url = HyperlinkedIdentityField(lookup_field='slug', view_name='api-tag-detail')\n", 333 | " name = CharField(max_length=31, validators=[])\n", 334 | " slug = SlugField(help_text='A label for URL config.', read_only=True)" 335 | ] 336 | }, 337 | "execution_count": 19, 338 | "metadata": {}, 339 | "output_type": "execute_result" 340 | } 341 | ], 342 | "source": [ 343 | "s_tag # note the \"read_only\" value in the SlugField below" 344 | ] 345 | }, 346 | { 347 | "cell_type": "markdown", 348 | "metadata": {}, 349 | "source": [ 350 | "## Notebook Cleanup" 351 | ] 352 | }, 353 | { 354 | "cell_type": "code", 355 | "execution_count": 20, 356 | "metadata": {}, 357 | "outputs": [], 358 | "source": [ 359 | "tag.name = 'django'\n", 360 | "tag.save()" 361 | ] 362 | } 363 | ], 364 | "metadata": { 365 | "kernelspec": { 366 | "display_name": "Django Shell-Plus", 367 | "language": "python", 368 | "name": "django_extensions" 369 | }, 370 | "language_info": { 371 | "codemirror_mode": { 372 | "name": "ipython", 373 | "version": 3 374 | }, 375 | "file_extension": ".py", 376 | "mimetype": "text/x-python", 377 | "name": "python", 378 | "nbconvert_exporter": "python", 379 | "pygments_lexer": "ipython3", 380 | "version": "3.7.0" 381 | } 382 | }, 383 | "nbformat": 4, 384 | "nbformat_minor": 2 385 | } 386 | -------------------------------------------------------------------------------- /src/Lesson_05.05_save_data_via_startup_serializer.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from datetime import date\n", 10 | "from pprint import pprint\n", 11 | "\n", 12 | "from django.test import RequestFactory\n", 13 | "\n", 14 | "from organizer.models import Startup, Tag\n", 15 | "from organizer.serializers import StartupSerializer" 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "execution_count": null, 21 | "metadata": {}, 22 | "outputs": [], 23 | "source": [ 24 | "# ensure we have a clean state\n", 25 | "Tag.objects.filter(name__endswith=' tag').delete()\n", 26 | "Startup.objects.filter(slug=\"jambon-software-llc\").delete()" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": null, 32 | "metadata": {}, 33 | "outputs": [], 34 | "source": [ 35 | "s_startup = StartupSerializer(\n", 36 | " data=dict(\n", 37 | " name=\"JamBon Software LLC\",\n", 38 | " slug=\"jambon-software-llc\",\n", 39 | " description=\"Consulting & training for web and mobile products.\",\n", 40 | " founded_date=date(2013, 1, 13),\n", 41 | " contact=\"django@jambonsw.com\", # not a real email\n", 42 | " website=\"https://jambonsw.com\",\n", 43 | " tags=[\n", 44 | " {\"name\": \"newest tag\"},\n", 45 | " ],\n", 46 | " ),\n", 47 | " context={\"request\": RequestFactory().get(\"/api/v1/startup/\")},\n", 48 | ")" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": null, 54 | "metadata": {}, 55 | "outputs": [], 56 | "source": [ 57 | "if s_startup.is_valid():\n", 58 | " try:\n", 59 | " startup = s_startup.save()\n", 60 | " except AssertionError as err:\n", 61 | " print(err)\n", 62 | " else:\n", 63 | " print(startup.pk)\n", 64 | " print(startup.name)\n", 65 | " print(startup.tags.all())\n", 66 | "else:\n", 67 | " pprint(s_startup.errors)" 68 | ] 69 | } 70 | ], 71 | "metadata": { 72 | "kernelspec": { 73 | "display_name": "Django Shell-Plus", 74 | "language": "python", 75 | "name": "django_extensions" 76 | }, 77 | "language_info": { 78 | "codemirror_mode": { 79 | "name": "ipython", 80 | "version": 3 81 | }, 82 | "file_extension": ".py", 83 | "mimetype": "text/x-python", 84 | "name": "python", 85 | "nbconvert_exporter": "python", 86 | "pygments_lexer": "ipython3", 87 | "version": "3.7.0" 88 | } 89 | }, 90 | "nbformat": 4, 91 | "nbformat_minor": 2 92 | } 93 | -------------------------------------------------------------------------------- /src/Lesson_06.01_django_forms_in_python.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Django Forms in Python" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "from pprint import pprint\n", 17 | "from organizer.forms import TagForm" 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": {}, 23 | "source": [ 24 | "## The Very Basics" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 2, 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [ 33 | "unbounded_form = TagForm()" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": 3, 39 | "metadata": {}, 40 | "outputs": [ 41 | { 42 | "data": { 43 | "text/plain": [ 44 | "False" 45 | ] 46 | }, 47 | "execution_count": 3, 48 | "metadata": {}, 49 | "output_type": "execute_result" 50 | } 51 | ], 52 | "source": [ 53 | "unbounded_form.is_bound # no data" 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": 4, 59 | "metadata": {}, 60 | "outputs": [ 61 | { 62 | "data": { 63 | "text/plain": [ 64 | "False" 65 | ] 66 | }, 67 | "execution_count": 4, 68 | "metadata": {}, 69 | "output_type": "execute_result" 70 | } 71 | ], 72 | "source": [ 73 | "unbounded_form.is_valid() # data is not valid" 74 | ] 75 | }, 76 | { 77 | "cell_type": "markdown", 78 | "metadata": {}, 79 | "source": [ 80 | "## Adding Data to the Mix" 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": 5, 86 | "metadata": {}, 87 | "outputs": [], 88 | "source": [ 89 | "bounded_form = TagForm({})" 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": 6, 95 | "metadata": {}, 96 | "outputs": [ 97 | { 98 | "data": { 99 | "text/plain": [ 100 | "True" 101 | ] 102 | }, 103 | "execution_count": 6, 104 | "metadata": {}, 105 | "output_type": "execute_result" 106 | } 107 | ], 108 | "source": [ 109 | "bounded_form.is_bound" 110 | ] 111 | }, 112 | { 113 | "cell_type": "code", 114 | "execution_count": 7, 115 | "metadata": {}, 116 | "outputs": [ 117 | { 118 | "name": "stdout", 119 | "output_type": "stream", 120 | "text": [ 121 | "'TagForm' object has no attribute 'cleaned_data'\n" 122 | ] 123 | } 124 | ], 125 | "source": [ 126 | "try:\n", 127 | " bounded_form.cleaned_data\n", 128 | "except AttributeError as err:\n", 129 | " print(err)" 130 | ] 131 | }, 132 | { 133 | "cell_type": "code", 134 | "execution_count": 8, 135 | "metadata": {}, 136 | "outputs": [ 137 | { 138 | "data": { 139 | "text/plain": [ 140 | "False" 141 | ] 142 | }, 143 | "execution_count": 8, 144 | "metadata": {}, 145 | "output_type": "execute_result" 146 | } 147 | ], 148 | "source": [ 149 | "bounded_form.is_valid()" 150 | ] 151 | }, 152 | { 153 | "cell_type": "code", 154 | "execution_count": 9, 155 | "metadata": {}, 156 | "outputs": [ 157 | { 158 | "data": { 159 | "text/plain": [ 160 | "{'slug': ''}" 161 | ] 162 | }, 163 | "execution_count": 9, 164 | "metadata": {}, 165 | "output_type": "execute_result" 166 | } 167 | ], 168 | "source": [ 169 | "bounded_form.cleaned_data # created by is_valid()" 170 | ] 171 | }, 172 | { 173 | "cell_type": "code", 174 | "execution_count": 10, 175 | "metadata": {}, 176 | "outputs": [], 177 | "source": [ 178 | "tagdata = {\n", 179 | " 'name':'django 2.0',\n", 180 | "}\n", 181 | "tform = TagForm(tagdata)" 182 | ] 183 | }, 184 | { 185 | "cell_type": "code", 186 | "execution_count": 11, 187 | "metadata": {}, 188 | "outputs": [ 189 | { 190 | "data": { 191 | "text/plain": [ 192 | "True" 193 | ] 194 | }, 195 | "execution_count": 11, 196 | "metadata": {}, 197 | "output_type": "execute_result" 198 | } 199 | ], 200 | "source": [ 201 | "tform.is_bound" 202 | ] 203 | }, 204 | { 205 | "cell_type": "code", 206 | "execution_count": 12, 207 | "metadata": {}, 208 | "outputs": [ 209 | { 210 | "data": { 211 | "text/plain": [ 212 | "True" 213 | ] 214 | }, 215 | "execution_count": 12, 216 | "metadata": {}, 217 | "output_type": "execute_result" 218 | } 219 | ], 220 | "source": [ 221 | "tform.is_valid()" 222 | ] 223 | }, 224 | { 225 | "cell_type": "code", 226 | "execution_count": 13, 227 | "metadata": {}, 228 | "outputs": [ 229 | { 230 | "data": { 231 | "text/plain": [ 232 | "{'name': 'django 2.0', 'slug': ''}" 233 | ] 234 | }, 235 | "execution_count": 13, 236 | "metadata": {}, 237 | "output_type": "execute_result" 238 | } 239 | ], 240 | "source": [ 241 | "tform.cleaned_data # slug shows, but is empty!" 242 | ] 243 | }, 244 | { 245 | "cell_type": "markdown", 246 | "metadata": {}, 247 | "source": [ 248 | "## Handling Validation Errors" 249 | ] 250 | }, 251 | { 252 | "cell_type": "code", 253 | "execution_count": 14, 254 | "metadata": {}, 255 | "outputs": [ 256 | { 257 | "data": { 258 | "text/plain": [ 259 | "{}" 260 | ] 261 | }, 262 | "execution_count": 14, 263 | "metadata": {}, 264 | "output_type": "execute_result" 265 | } 266 | ], 267 | "source": [ 268 | "# from the TagForm above\n", 269 | "tform.errors" 270 | ] 271 | }, 272 | { 273 | "cell_type": "code", 274 | "execution_count": 15, 275 | "metadata": {}, 276 | "outputs": [ 277 | { 278 | "data": { 279 | "text/plain": [ 280 | "{}" 281 | ] 282 | }, 283 | "execution_count": 15, 284 | "metadata": {}, 285 | "output_type": "execute_result" 286 | } 287 | ], 288 | "source": [ 289 | "tform = TagForm(tagdata)\n", 290 | "tform.errors" 291 | ] 292 | }, 293 | { 294 | "cell_type": "code", 295 | "execution_count": 16, 296 | "metadata": {}, 297 | "outputs": [ 298 | { 299 | "data": { 300 | "text/plain": [ 301 | "{'name': 'django 2.0', 'slug': ''}" 302 | ] 303 | }, 304 | "execution_count": 16, 305 | "metadata": {}, 306 | "output_type": "execute_result" 307 | } 308 | ], 309 | "source": [ 310 | "tform.cleaned_data # created by access to errors attribute" 311 | ] 312 | }, 313 | { 314 | "cell_type": "code", 315 | "execution_count": 17, 316 | "metadata": {}, 317 | "outputs": [], 318 | "source": [ 319 | "errordata = {\n", 320 | " 'name': None,\n", 321 | " 'slug':'new_tag',\n", 322 | "}\n", 323 | "tform = TagForm(errordata)" 324 | ] 325 | }, 326 | { 327 | "cell_type": "code", 328 | "execution_count": 18, 329 | "metadata": {}, 330 | "outputs": [ 331 | { 332 | "data": { 333 | "text/plain": [ 334 | "True" 335 | ] 336 | }, 337 | "execution_count": 18, 338 | "metadata": {}, 339 | "output_type": "execute_result" 340 | } 341 | ], 342 | "source": [ 343 | "tform.is_bound" 344 | ] 345 | }, 346 | { 347 | "cell_type": "code", 348 | "execution_count": 19, 349 | "metadata": {}, 350 | "outputs": [ 351 | { 352 | "data": { 353 | "text/plain": [ 354 | "False" 355 | ] 356 | }, 357 | "execution_count": 19, 358 | "metadata": {}, 359 | "output_type": "execute_result" 360 | } 361 | ], 362 | "source": [ 363 | "tform.is_valid()" 364 | ] 365 | }, 366 | { 367 | "cell_type": "code", 368 | "execution_count": 20, 369 | "metadata": {}, 370 | "outputs": [ 371 | { 372 | "data": { 373 | "text/plain": [ 374 | "{'slug': 'new_tag'}" 375 | ] 376 | }, 377 | "execution_count": 20, 378 | "metadata": {}, 379 | "output_type": "execute_result" 380 | } 381 | ], 382 | "source": [ 383 | "tform.cleaned_data" 384 | ] 385 | }, 386 | { 387 | "cell_type": "code", 388 | "execution_count": 21, 389 | "metadata": {}, 390 | "outputs": [ 391 | { 392 | "data": { 393 | "text/plain": [ 394 | "{'name': ['This field is required.']}" 395 | ] 396 | }, 397 | "execution_count": 21, 398 | "metadata": {}, 399 | "output_type": "execute_result" 400 | } 401 | ], 402 | "source": [ 403 | "tform.errors" 404 | ] 405 | }, 406 | { 407 | "cell_type": "code", 408 | "execution_count": 22, 409 | "metadata": {}, 410 | "outputs": [ 411 | { 412 | "name": "stdout", 413 | "output_type": "stream", 414 | "text": [ 415 | "{'name': [ValidationError(['This field is required.'])]}\n" 416 | ] 417 | } 418 | ], 419 | "source": [ 420 | "# normally lazily evaluates\n", 421 | "# pprint helps make this much clearer\n", 422 | "# (try it without pprint yourself!)\n", 423 | "pprint(tform.errors.as_data())" 424 | ] 425 | }, 426 | { 427 | "cell_type": "code", 428 | "execution_count": 23, 429 | "metadata": {}, 430 | "outputs": [], 431 | "source": [ 432 | "errordata2 = {\n", 433 | " 'name':'abcdefghijklmnopqrstuvwxyzabcdef',\n", 434 | " 'slug':'new_tag',\n", 435 | "}" 436 | ] 437 | }, 438 | { 439 | "cell_type": "code", 440 | "execution_count": 24, 441 | "metadata": {}, 442 | "outputs": [ 443 | { 444 | "data": { 445 | "text/plain": [ 446 | "32" 447 | ] 448 | }, 449 | "execution_count": 24, 450 | "metadata": {}, 451 | "output_type": "execute_result" 452 | } 453 | ], 454 | "source": [ 455 | "len(errordata2['name'])" 456 | ] 457 | }, 458 | { 459 | "cell_type": "code", 460 | "execution_count": 25, 461 | "metadata": {}, 462 | "outputs": [ 463 | { 464 | "data": { 465 | "text/plain": [ 466 | "False" 467 | ] 468 | }, 469 | "execution_count": 25, 470 | "metadata": {}, 471 | "output_type": "execute_result" 472 | } 473 | ], 474 | "source": [ 475 | "tform = TagForm(errordata2)\n", 476 | "tform.is_valid()" 477 | ] 478 | }, 479 | { 480 | "cell_type": "code", 481 | "execution_count": 26, 482 | "metadata": {}, 483 | "outputs": [ 484 | { 485 | "data": { 486 | "text/plain": [ 487 | "{'slug': 'new_tag'}" 488 | ] 489 | }, 490 | "execution_count": 26, 491 | "metadata": {}, 492 | "output_type": "execute_result" 493 | } 494 | ], 495 | "source": [ 496 | "tform.cleaned_data" 497 | ] 498 | }, 499 | { 500 | "cell_type": "code", 501 | "execution_count": 27, 502 | "metadata": { 503 | "scrolled": true 504 | }, 505 | "outputs": [ 506 | { 507 | "data": { 508 | "text/plain": [ 509 | "{'name': ['Ensure this value has at most 31 characters (it has 32).']}" 510 | ] 511 | }, 512 | "execution_count": 27, 513 | "metadata": {}, 514 | "output_type": "execute_result" 515 | } 516 | ], 517 | "source": [ 518 | "tform.errors" 519 | ] 520 | } 521 | ], 522 | "metadata": { 523 | "kernelspec": { 524 | "display_name": "Django Shell-Plus", 525 | "language": "python", 526 | "name": "django_extensions" 527 | }, 528 | "language_info": { 529 | "codemirror_mode": { 530 | "name": "ipython", 531 | "version": 3 532 | }, 533 | "file_extension": ".py", 534 | "mimetype": "text/x-python", 535 | "name": "python", 536 | "nbconvert_exporter": "python", 537 | "pygments_lexer": "ipython3", 538 | "version": "3.7.0" 539 | } 540 | }, 541 | "nbformat": 4, 542 | "nbformat_minor": 1 543 | } 544 | -------------------------------------------------------------------------------- /src/Lesson_06.02_model_validation.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Model Validation\n", 8 | "\n", 9 | "By default, Django does not use Model Validation (unless we use `ModelForm`).\n", 10 | "\n", 11 | "See: https://docs.djangoproject.com/en/2.1/ref/models/instances/#validating-objects" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 1, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "from datetime import date\n", 21 | "from blog.models import Post" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 2, 27 | "metadata": {}, 28 | "outputs": [ 29 | { 30 | "data": { 31 | "text/plain": [ 32 | "" 33 | ] 34 | }, 35 | "execution_count": 2, 36 | "metadata": {}, 37 | "output_type": "execute_result" 38 | } 39 | ], 40 | "source": [ 41 | "# cleaup section below may be helpful!\n", 42 | "training_post = Post.objects.create(\n", 43 | " title='Django Training',\n", 44 | " slug='django-training',\n", 45 | " pub_date=date(2013, 1, 18),\n", 46 | " text=(\n", 47 | " \"Learn Django in a classroom setting \"\n", 48 | " \"with JamBon Software.\"),\n", 49 | ")\n", 50 | "training_post" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": 3, 56 | "metadata": {}, 57 | "outputs": [], 58 | "source": [ 59 | "# uh-oh, we now have two Posts with the same URI!\n", 60 | "conflict = Post.objects.create(\n", 61 | " title='Conflict',\n", 62 | " slug=training_post.slug,\n", 63 | " text=\"This object will cause problems.\",\n", 64 | " pub_date=training_post.pub_date\n", 65 | ")\n", 66 | "# what happens if you try to use get_or_create above?" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": 4, 72 | "metadata": {}, 73 | "outputs": [ 74 | { 75 | "data": { 76 | "text/plain": [ 77 | ", ]>" 78 | ] 79 | }, 80 | "execution_count": 4, 81 | "metadata": {}, 82 | "output_type": "execute_result" 83 | } 84 | ], 85 | "source": [ 86 | "Post.objects.all()" 87 | ] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "execution_count": 5, 92 | "metadata": {}, 93 | "outputs": [ 94 | { 95 | "name": "stdout", 96 | "output_type": "stream", 97 | "text": [ 98 | "get() returned more than one Post -- it returned 2!\n" 99 | ] 100 | } 101 | ], 102 | "source": [ 103 | "# this exception will be displayed at http://127.0.0.1:8000/blog/2013/1/django-training/\n", 104 | "try:\n", 105 | " Post.objects.get(\n", 106 | " pub_date__year=2013,\n", 107 | " pub_date__month=1,\n", 108 | " slug='django-training'\n", 109 | " )\n", 110 | "except Post.MultipleObjectsReturned as e:\n", 111 | " print(str(e))" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": 6, 117 | "metadata": {}, 118 | "outputs": [], 119 | "source": [ 120 | "conflict2 = Post(\n", 121 | " title='Conflict 2: The Return',\n", 122 | " slug=training_post.slug,\n", 123 | " text='More Problem Behavior in Theaters Soon!',\n", 124 | " pub_date=training_post.pub_date\n", 125 | ")" 126 | ] 127 | }, 128 | { 129 | "cell_type": "code", 130 | "execution_count": 7, 131 | "metadata": {}, 132 | "outputs": [ 133 | { 134 | "data": { 135 | "text/plain": [ 136 | "" 137 | ] 138 | }, 139 | "execution_count": 7, 140 | "metadata": {}, 141 | "output_type": "execute_result" 142 | } 143 | ], 144 | "source": [ 145 | "conflict2" 146 | ] 147 | }, 148 | { 149 | "cell_type": "code", 150 | "execution_count": 8, 151 | "metadata": {}, 152 | "outputs": [ 153 | { 154 | "name": "stdout", 155 | "output_type": "stream", 156 | "text": [ 157 | "{'slug': ['Slug must be unique for Date published month.']}\n" 158 | ] 159 | } 160 | ], 161 | "source": [ 162 | "from django.core.exceptions import ValidationError\n", 163 | "try:\n", 164 | " conflict2.full_clean()\n", 165 | "except ValidationError as err:\n", 166 | " print(str(err))" 167 | ] 168 | }, 169 | { 170 | "cell_type": "markdown", 171 | "metadata": {}, 172 | "source": [ 173 | "# Cleanup" 174 | ] 175 | }, 176 | { 177 | "cell_type": "code", 178 | "execution_count": 9, 179 | "metadata": {}, 180 | "outputs": [ 181 | { 182 | "data": { 183 | "text/plain": [ 184 | "(2, {'blog.Post_tags': 0, 'blog.Post_startups': 0, 'blog.Post': 2})" 185 | ] 186 | }, 187 | "execution_count": 9, 188 | "metadata": {}, 189 | "output_type": "execute_result" 190 | } 191 | ], 192 | "source": [ 193 | "Post.objects.filter(\n", 194 | " pub_date__year=2013,\n", 195 | " pub_date__month=1,\n", 196 | " slug='django-training'\n", 197 | ").delete()" 198 | ] 199 | } 200 | ], 201 | "metadata": { 202 | "kernelspec": { 203 | "display_name": "Django Shell-Plus", 204 | "language": "python", 205 | "name": "django_extensions" 206 | }, 207 | "language_info": { 208 | "codemirror_mode": { 209 | "name": "ipython", 210 | "version": 3 211 | }, 212 | "file_extension": ".py", 213 | "mimetype": "text/x-python", 214 | "name": "python", 215 | "nbconvert_exporter": "python", 216 | "pygments_lexer": "ipython3", 217 | "version": "3.7.0" 218 | } 219 | }, 220 | "nbformat": 4, 221 | "nbformat_minor": 1 222 | } 223 | -------------------------------------------------------------------------------- /src/blog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jambonrose/python-web-dev-21-2/ab8bd1e8a394e5dca1f14b821dc5344594c172f4/src/blog/__init__.py -------------------------------------------------------------------------------- /src/blog/admin.py: -------------------------------------------------------------------------------- 1 | """Configuration of Blog Admin panel""" 2 | from django.contrib import admin 3 | 4 | from .models import Post 5 | 6 | admin.site.register(Post) 7 | -------------------------------------------------------------------------------- /src/blog/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BlogConfig(AppConfig): 5 | name = "blog" 6 | -------------------------------------------------------------------------------- /src/blog/forms.py: -------------------------------------------------------------------------------- 1 | """Forms for the Blog app""" 2 | from django.forms import ModelForm 3 | 4 | from .models import Post 5 | 6 | 7 | class PostForm(ModelForm): 8 | """HTML form for Post objects""" 9 | 10 | class Meta: 11 | model = Post 12 | fields = "__all__" 13 | -------------------------------------------------------------------------------- /src/blog/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1 on 2018-08-05 00:57 2 | 3 | import datetime 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [("organizer", "0001_initial")] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name="Post", 17 | fields=[ 18 | ( 19 | "id", 20 | models.AutoField( 21 | auto_created=True, 22 | primary_key=True, 23 | serialize=False, 24 | verbose_name="ID", 25 | ), 26 | ), 27 | ("title", models.CharField(max_length=63)), 28 | ( 29 | "slug", 30 | models.SlugField( 31 | help_text="A label for URL config", 32 | max_length=63, 33 | unique_for_month="pub_date", 34 | ), 35 | ), 36 | ("text", models.TextField()), 37 | ( 38 | "pub_date", 39 | models.DateField( 40 | default=datetime.date.today, 41 | verbose_name="date published", 42 | ), 43 | ), 44 | ( 45 | "startups", 46 | models.ManyToManyField( 47 | related_name="blog_posts", 48 | to="organizer.Startup", 49 | ), 50 | ), 51 | ( 52 | "tags", 53 | models.ManyToManyField( 54 | related_name="blog_posts", 55 | to="organizer.Tag", 56 | ), 57 | ), 58 | ], 59 | options={ 60 | "verbose_name": "blog post", 61 | "ordering": ["-pub_date", "title"], 62 | "get_latest_by": "pub_date", 63 | }, 64 | ) 65 | ] 66 | -------------------------------------------------------------------------------- /src/blog/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jambonrose/python-web-dev-21-2/ab8bd1e8a394e5dca1f14b821dc5344594c172f4/src/blog/migrations/__init__.py -------------------------------------------------------------------------------- /src/blog/models.py: -------------------------------------------------------------------------------- 1 | """Django data models for news 2 | 3 | Django Model Documentation: 4 | https://docs.djangoproject.com/en/2.1/topics/db/models/ 5 | https://docs.djangoproject.com/en/2.1/ref/models/options/ 6 | https://docs.djangoproject.com/en/2.1/internals/contributing/writing-code/coding-style/#model-style 7 | Django Field Reference: 8 | https://docs.djangoproject.com/en/2.1/ref/models/fields/ 9 | https://docs.djangoproject.com/en/2.1/ref/models/fields/#charfield 10 | https://docs.djangoproject.com/en/2.1/ref/models/fields/#datefield 11 | https://docs.djangoproject.com/en/2.1/ref/models/fields/#manytomanyfield 12 | https://docs.djangoproject.com/en/2.1/ref/models/fields/#slugfield 13 | https://docs.djangoproject.com/en/2.1/ref/models/fields/#textfield 14 | 15 | """ 16 | from datetime import date 17 | 18 | from django.db.models import ( 19 | CharField, 20 | DateField, 21 | ManyToManyField, 22 | Model, 23 | SlugField, 24 | TextField, 25 | ) 26 | from django.urls import reverse 27 | 28 | from organizer.models import Startup, Tag 29 | 30 | 31 | class Post(Model): 32 | """Blog post; news article about startups""" 33 | 34 | title = CharField(max_length=63) 35 | slug = SlugField( 36 | max_length=63, 37 | help_text="A label for URL config", 38 | unique_for_month="pub_date", 39 | ) 40 | text = TextField() 41 | pub_date = DateField( 42 | "date published", default=date.today 43 | ) 44 | tags = ManyToManyField(Tag, related_name="blog_posts") 45 | startups = ManyToManyField( 46 | Startup, related_name="blog_posts" 47 | ) 48 | 49 | class Meta: 50 | get_latest_by = "pub_date" 51 | ordering = ["-pub_date", "title"] 52 | verbose_name = "blog post" 53 | 54 | def __str__(self): 55 | date_string = self.pub_date.strftime("%Y-%m-%d") 56 | return f"{self.title} on {date_string}" 57 | 58 | def get_absolute_url(self): 59 | """Return URL to detail page of Post""" 60 | return reverse( 61 | "post_detail", 62 | kwargs={ 63 | "year": self.pub_date.year, 64 | "month": self.pub_date.month, 65 | "slug": self.slug, 66 | }, 67 | ) 68 | 69 | def get_update_url(self): 70 | """Return URL to update page of Post""" 71 | return reverse( 72 | "post_update", 73 | kwargs={ 74 | "year": self.pub_date.year, 75 | "month": self.pub_date.month, 76 | "slug": self.slug, 77 | }, 78 | ) 79 | 80 | def get_delete_url(self): 81 | """Return URL to delete page of Post""" 82 | return reverse( 83 | "post_delete", 84 | kwargs={ 85 | "year": self.pub_date.year, 86 | "month": self.pub_date.month, 87 | "slug": self.slug, 88 | }, 89 | ) 90 | -------------------------------------------------------------------------------- /src/blog/routers.py: -------------------------------------------------------------------------------- 1 | """URL Paths and Routers for Blog App""" 2 | from rest_framework.routers import SimpleRouter 3 | 4 | from .viewsets import PostViewSet 5 | 6 | 7 | class PostRouter(SimpleRouter): 8 | """Override the SimpleRouter for blog posts 9 | 10 | DRF's routers expect there to only be a single variable 11 | for finding objects. However, our blog posts needs 12 | three! We therefore override the Router's behavior to 13 | make it do what we want. 14 | 15 | The big question: was it worth switching to a ViewSet 16 | and Router over our previous config for this? 17 | """ 18 | 19 | def get_lookup_regex(self, *args, **kwargs): 20 | """Return regular expression pattern for URL path 21 | 22 | This is the equivalent of the simple path: 23 | // 24 | """ 25 | return ( 26 | r"(?P\d+)/" 27 | r"(?P\d+)/" 28 | r"(?P[\w\-]+)" 29 | ) 30 | 31 | 32 | post_router = PostRouter() 33 | post_router.register( 34 | "blog", PostViewSet, base_name="api-post" 35 | ) 36 | urlpatterns = post_router.urls 37 | -------------------------------------------------------------------------------- /src/blog/serializers.py: -------------------------------------------------------------------------------- 1 | """Serializers for th Blog App 2 | 3 | Serializer Documentation 4 | http://www.django-rest-framework.org/api-guide/serializers/ 5 | http://www.django-rest-framework.org/api-guide/fields/ 6 | http://www.django-rest-framework.org/api-guide/relations/ 7 | """ 8 | 9 | from rest_framework.reverse import reverse 10 | from rest_framework.serializers import ( 11 | HyperlinkedRelatedField, 12 | ModelSerializer, 13 | SerializerMethodField, 14 | ) 15 | 16 | from organizer.models import Startup, Tag 17 | 18 | from .models import Post 19 | 20 | 21 | class PostSerializer(ModelSerializer): 22 | """Serialize Post data""" 23 | 24 | url = SerializerMethodField() 25 | tags = HyperlinkedRelatedField( 26 | lookup_field="slug", 27 | many=True, 28 | queryset=Tag.objects.all(), 29 | view_name="api-tag-detail", 30 | ) 31 | startups = HyperlinkedRelatedField( 32 | lookup_field="slug", 33 | many=True, 34 | queryset=Startup.objects.all(), 35 | view_name="api-startup-detail", 36 | ) 37 | 38 | class Meta: 39 | model = Post 40 | exclude = ("id",) 41 | 42 | def get_url(self, post): 43 | """Return full API URL for serialized POST object""" 44 | return reverse( 45 | "api-post-detail", 46 | kwargs=dict( 47 | year=post.pub_date.year, 48 | month=post.pub_date.month, 49 | slug=post.slug, 50 | ), 51 | request=self.context["request"], 52 | ) 53 | -------------------------------------------------------------------------------- /src/blog/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /src/blog/urls.py: -------------------------------------------------------------------------------- 1 | """URL paths for Blog App""" 2 | from django.urls import path 3 | 4 | from .views import ( 5 | PostCreate, 6 | PostDelete, 7 | PostDetail, 8 | PostList, 9 | PostUpdate, 10 | ) 11 | 12 | urlpatterns = [ 13 | path("", PostList.as_view(), name="post_list"), 14 | path( 15 | "create/", PostCreate.as_view(), name="post_create" 16 | ), 17 | path( 18 | "///", 19 | PostDetail.as_view(), 20 | name="post_detail", 21 | ), 22 | path( 23 | "///delete/", 24 | PostDelete.as_view(), 25 | name="post_delete", 26 | ), 27 | path( 28 | "///update/", 29 | PostUpdate.as_view(), 30 | name="post_update", 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /src/blog/views.py: -------------------------------------------------------------------------------- 1 | """Views for Blog App""" 2 | from django.contrib.auth.mixins import LoginRequiredMixin 3 | from django.shortcuts import get_object_or_404 4 | from django.urls import reverse_lazy 5 | from django.views.generic import ( 6 | CreateView, 7 | DeleteView, 8 | DetailView, 9 | ListView, 10 | UpdateView, 11 | ) 12 | 13 | from .forms import PostForm 14 | from .models import Post 15 | 16 | 17 | class PostObjectMixin: 18 | """Django View mix-in to find blog posts""" 19 | 20 | model = Post 21 | 22 | def get_object(self, queryset=None): 23 | """Get a blog post using year, month, and slug 24 | 25 | http://ccbv.co.uk/SingleObjectMixin 26 | """ 27 | if queryset is None: 28 | queryset = self.get_queryset() 29 | 30 | year, month, slug = map( 31 | self.kwargs.get, ["year", "month", "slug"] 32 | ) 33 | if any(arg is None for arg in (year, month, slug)): 34 | raise AttributeError( 35 | f"View {self.__class__.__name__} must be" 36 | f"called with year, month, and slug for" 37 | f"Post objects" 38 | ) 39 | return get_object_or_404( 40 | queryset, 41 | pub_date__year=year, 42 | pub_date__month=month, 43 | slug=slug, 44 | ) 45 | 46 | 47 | class PostCreate(LoginRequiredMixin, CreateView): 48 | """Create new blog posts""" 49 | 50 | form_class = PostForm 51 | model = Post 52 | template_name = "post/form.html" 53 | extra_context = {"update": False} 54 | 55 | 56 | class PostDetail(PostObjectMixin, DetailView): 57 | """Display a single blog Post""" 58 | 59 | template_name = "post/detail.html" 60 | 61 | 62 | class PostDelete( 63 | PostObjectMixin, LoginRequiredMixin, DeleteView 64 | ): 65 | """Delete a single blog post""" 66 | 67 | template_name = "post/confirm_delete.html" 68 | success_url = reverse_lazy("post_list") 69 | 70 | 71 | class PostList(ListView): 72 | """Display a list of blog Posts""" 73 | 74 | model = Post 75 | template_name = "post/list.html" 76 | 77 | 78 | class PostUpdate( 79 | PostObjectMixin, LoginRequiredMixin, UpdateView 80 | ): 81 | """Update existing blog posts""" 82 | 83 | form_class = PostForm 84 | template_name = "post/form.html" 85 | extra_context = {"update": True} 86 | -------------------------------------------------------------------------------- /src/blog/viewsets.py: -------------------------------------------------------------------------------- 1 | """Viewsets for the Blog app""" 2 | from django.shortcuts import get_object_or_404 3 | from rest_framework.viewsets import ModelViewSet 4 | 5 | from .models import Post 6 | from .serializers import PostSerializer 7 | 8 | 9 | class PostViewSet(ModelViewSet): 10 | """A set of views for Post model""" 11 | 12 | queryset = Post.objects.all() 13 | serializer_class = PostSerializer 14 | 15 | def get_object(self): 16 | """Override DRF's generic method 17 | 18 | http://www.cdrf.co/3.7/rest_framework.viewsets/ModelViewSet.html#get_object 19 | """ 20 | month = self.kwargs.get("month") 21 | year = self.kwargs.get("year") 22 | slug = self.kwargs.get("slug") 23 | 24 | queryset = self.filter_queryset(self.get_queryset()) 25 | 26 | post = get_object_or_404( 27 | queryset, 28 | pub_date__year=year, 29 | pub_date__month=month, 30 | slug=slug, 31 | ) 32 | self.check_object_permissions(self.request, post) 33 | return post 34 | -------------------------------------------------------------------------------- /src/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jambonrose/python-web-dev-21-2/ab8bd1e8a394e5dca1f14b821dc5344594c172f4/src/config/__init__.py -------------------------------------------------------------------------------- /src/config/settings/base.py: -------------------------------------------------------------------------------- 1 | """Django settings for Startup Organizer Project 2 | 3 | Built during Andrew Pinkham's class on Safari Books Online. 4 | 5 | https://docs.djangoproject.com/en/2.1/topics/settings/ 6 | https://docs.djangoproject.com/en/2.1/ref/settings/ 7 | https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ 8 | """ 9 | 10 | from environ import Env, Path 11 | 12 | ENV = Env() 13 | 14 | BASE_DIR = Path(__file__) - 3 15 | 16 | SECRET_KEY = ENV.str("SECRET_KEY") 17 | 18 | DEBUG = ENV.bool("DEBUG", default=False) 19 | 20 | ALLOWED_HOSTS = [] 21 | 22 | # Application definition 23 | 24 | INSTALLED_APPS = [ 25 | "whitenoise.runserver_nostatic", 26 | "django.contrib.admin", 27 | "django.contrib.auth", 28 | "django.contrib.contenttypes", 29 | "django.contrib.sessions", 30 | "django.contrib.messages", 31 | "django.contrib.staticfiles", 32 | # third party 33 | "django_extensions", 34 | "rest_framework", 35 | "url_checks.apps.UrlChecksConfig", 36 | # first party 37 | "blog.apps.BlogConfig", 38 | "organizer.apps.OrganizerConfig", 39 | ] 40 | 41 | MIDDLEWARE = [ 42 | "django.middleware.security.SecurityMiddleware", 43 | "whitenoise.middleware.WhiteNoiseMiddleware", 44 | "django.contrib.sessions.middleware.SessionMiddleware", 45 | "django.middleware.common.CommonMiddleware", 46 | "django.middleware.csrf.CsrfViewMiddleware", 47 | "django.contrib.auth.middleware.AuthenticationMiddleware", 48 | "django.contrib.messages.middleware.MessageMiddleware", 49 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 50 | ] 51 | 52 | ROOT_URLCONF = "config.urls" 53 | 54 | TEMPLATES = [ 55 | { 56 | "BACKEND": "django.template.backends.django.DjangoTemplates", 57 | "DIRS": [BASE_DIR("templates")], 58 | "APP_DIRS": True, 59 | "OPTIONS": { 60 | "context_processors": [ 61 | "django.template.context_processors.debug", 62 | "django.template.context_processors.request", 63 | "django.contrib.auth.context_processors.auth", 64 | "django.contrib.messages.context_processors.messages", 65 | ] 66 | }, 67 | } 68 | ] 69 | 70 | WSGI_APPLICATION = "config.wsgi.application" 71 | 72 | 73 | # Database 74 | # https://docs.djangoproject.com/en/2.1/ref/settings/#databases 75 | 76 | DATABASES = { 77 | "default": ENV.db( 78 | "DATABASE_URL", 79 | default=f"sqlite:////{BASE_DIR}/db.sqlite3", 80 | ) 81 | } 82 | 83 | # Password validation 84 | # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators 85 | 86 | AUTH_PASSWORD_VALIDATORS = [ 87 | { 88 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" 89 | }, 90 | { 91 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator" 92 | }, 93 | { 94 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator" 95 | }, 96 | { 97 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator" 98 | }, 99 | ] 100 | 101 | 102 | # Internationalization 103 | # https://docs.djangoproject.com/en/2.1/topics/i18n/ 104 | 105 | LANGUAGE_CODE = "en-us" 106 | 107 | TIME_ZONE = "UTC" 108 | 109 | USE_I18N = True 110 | 111 | USE_L10N = True 112 | 113 | USE_TZ = True 114 | 115 | 116 | # Static files (CSS, JavaScript, Images) 117 | # https://docs.djangoproject.com/en/2.1/howto/static-files/ 118 | 119 | STATIC_ROOT = BASE_DIR("runtime", "static") 120 | STATIC_URL = "/static/" 121 | STATICFILES_STORAGE = ( 122 | "django.contrib.staticfiles.storage.StaticFilesStorage" 123 | ) 124 | 125 | NOTEBOOK_ARGUMENTS = [ 126 | "--ip", 127 | "0.0.0.0", 128 | "--allow-root", 129 | "--no-browser", 130 | ] 131 | -------------------------------------------------------------------------------- /src/config/settings/development.py: -------------------------------------------------------------------------------- 1 | """Development settings for Startup Organizer""" 2 | from .base import * # noqa: F403 3 | 4 | DEBUG = ENV.bool("DEBUG", default=True) # noqa: F405 5 | 6 | TEMPLATES[0]["OPTIONS"].update( # noqa: F405 7 | { 8 | "debug": ENV.bool( # noqa: F405 9 | "TEMPLATE_DEBUG", default=True 10 | ) 11 | } 12 | ) 13 | 14 | # https://github.com/evansd/whitenoise/issues/191 15 | # Normally set to settings.DEBUG, but tests run with DEBUG=FALSE! 16 | WHITENOISE_AUTOREFRESH = True 17 | WHITENOISE_USE_FINDERS = True 18 | -------------------------------------------------------------------------------- /src/config/settings/production.py: -------------------------------------------------------------------------------- 1 | """Django Settings for Production instances of the site""" 2 | from .base import * # noqa: F401 F403 3 | 4 | ###################################################################### 5 | # PRODUCTION SETTINGS 6 | ###################################################################### 7 | 8 | ALLOWED_HOSTS = [".herokuapp.com"] 9 | 10 | ADMINS = MANAGERS = [ 11 | # ('Your Name', 'name@email.com'), 12 | ] 13 | 14 | STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" 15 | 16 | ##################### 17 | # SECURITY SETTINGS # 18 | ##################### 19 | 20 | CSRF_COOKIE_HTTPONLY = True 21 | CSRF_COOKIE_SECURE = True 22 | 23 | SECURE_BROWSER_XSS_FILTER = True 24 | SECURE_CONTENT_TYPE_NOSNIFF = True 25 | 26 | SECURE_SSL_REDIRECT = True 27 | SECURE_HSTS_SECONDS = 3600 28 | SECURE_HSTS_INCLUDE_SUBDOMAINS = True 29 | 30 | SESSION_COOKIE_DOMAIN = None # not set on subdomains 31 | SESSION_COOKIE_HTTPONLY = True 32 | SESSION_COOKIE_NAME = "suorganizer_sessionid" 33 | SESSION_COOKIE_SECURE = True 34 | SESSION_EXPIRE_AT_BROWSER_CLOSE = True 35 | 36 | X_FRAME_OPTIONS = "DENY" 37 | 38 | REST_FRAMEWORK = { 39 | "DEFAULT_PERMISSION_CLASSES": ( 40 | "rest_framework.permissions.IsAuthenticatedOrReadOnly", 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /src/config/urls.py: -------------------------------------------------------------------------------- 1 | """Root URL Configuration for Startup Organizer Project""" 2 | from django.contrib import admin 3 | from django.urls import include, path 4 | from django.views.generic import TemplateView 5 | 6 | from blog import urls as blog_urls 7 | from blog.routers import urlpatterns as blog_api_urls 8 | from organizer import urls as organizer_urls 9 | from organizer.routers import ( 10 | urlpatterns as organizer_api_urls, 11 | ) 12 | 13 | from .views import RootApiView 14 | 15 | root_api_url = [ 16 | path("", RootApiView.as_view(), name="api-root") 17 | ] 18 | api_urls = root_api_url + blog_api_urls + organizer_api_urls 19 | 20 | urlpatterns = [ 21 | path("admin/", admin.site.urls), 22 | path("api/v1/", include(api_urls)), 23 | path("blog/", include(blog_urls)), 24 | path("", include(organizer_urls)), 25 | path( 26 | "", 27 | TemplateView.as_view(template_name="root.html"), 28 | name="site_root", 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /src/config/views.py: -------------------------------------------------------------------------------- 1 | """Site Views 2 | 3 | (Views that do not belong in any app but are needed by site.) 4 | 5 | I'm breaking the rules here for simplicity: this should not 6 | be in config, as this is not configuration for the project. 7 | However, introducing an app specifically for a single view 8 | would overcomplicate the work we're doing, so it's going 9 | here instead. 10 | """ 11 | 12 | from rest_framework.response import Response 13 | from rest_framework.reverse import reverse 14 | from rest_framework.status import HTTP_200_OK 15 | from rest_framework.views import APIView 16 | 17 | 18 | class RootApiView(APIView): 19 | """Direct users to other API endpoints""" 20 | 21 | def get(self, request, *args, **kwargs): 22 | """Build & display links to other endpoints""" 23 | api_endpoints = [ 24 | # (name, url_name), 25 | ("tag", "api-tag-list"), 26 | ("startup", "api-startup-list"), 27 | ("newslink", "api-newslink-list"), 28 | ("blog", "api-post-list"), 29 | ] 30 | data = { 31 | name: reverse( 32 | url_name, 33 | request=request, 34 | format=kwargs.get("format", None), 35 | ) 36 | for (name, url_name) in api_endpoints 37 | } 38 | return Response(data=data, status=HTTP_200_OK) 39 | -------------------------------------------------------------------------------- /src/config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for config 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/2.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault( 15 | "DJANGO_SETTINGS_MODULE", "config.settings" 16 | ) 17 | 18 | application = get_wsgi_application() 19 | -------------------------------------------------------------------------------- /src/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault( 7 | "DJANGO_SETTINGS_MODULE", 8 | "config.settings.development", 9 | ) 10 | try: 11 | from django.core.management import ( 12 | execute_from_command_line 13 | ) 14 | except ImportError as exc: 15 | raise ImportError( 16 | "Couldn't import Django. Are you sure it's installed and " 17 | "available on your PYTHONPATH environment variable? Did you " 18 | "forget to activate a virtual environment?" 19 | ) from exc 20 | execute_from_command_line(sys.argv) 21 | -------------------------------------------------------------------------------- /src/organizer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jambonrose/python-web-dev-21-2/ab8bd1e8a394e5dca1f14b821dc5344594c172f4/src/organizer/__init__.py -------------------------------------------------------------------------------- /src/organizer/admin.py: -------------------------------------------------------------------------------- 1 | """Configuration of Organizer Admin panel""" 2 | from django.contrib import admin 3 | 4 | from .models import NewsLink, Startup, Tag 5 | 6 | admin.site.register(NewsLink) 7 | 8 | 9 | @admin.register(Tag) 10 | class TagAdmin(admin.ModelAdmin): 11 | """Configure Tag panel""" 12 | 13 | list_display = ("name", "slug") 14 | 15 | 16 | @admin.register(Startup) 17 | class StartupAdmin(admin.ModelAdmin): 18 | """Configure Startup panel""" 19 | 20 | list_display = ("name", "slug") 21 | prepopulated_fields = {"slug": ("name",)} 22 | -------------------------------------------------------------------------------- /src/organizer/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class OrganizerConfig(AppConfig): 5 | name = "organizer" 6 | -------------------------------------------------------------------------------- /src/organizer/forms.py: -------------------------------------------------------------------------------- 1 | """Forms for the Organizer app""" 2 | from django.core.exceptions import ValidationError 3 | from django.forms import ModelForm 4 | from django.forms.widgets import HiddenInput 5 | 6 | from .models import NewsLink, Startup, Tag 7 | 8 | 9 | class LowercaseNameMixin: 10 | """Form cleaner to lower case of name field""" 11 | 12 | def clean_name(self): 13 | """Ensure Tag name is always lowercase""" 14 | return self.cleaned_data["name"].lower() 15 | 16 | 17 | class SlugCleanMixin: 18 | """Mixin class to ensure slug field is not create""" 19 | 20 | def clean_slug(self): 21 | """Ensure slug is not 'create' 22 | 23 | This is an oversimplification!!! See the following 24 | link for how to raise the error correctly. 25 | 26 | https://docs.djangoproject.com/en/2.1/ref/forms/validation/#raising-validationerror 27 | 28 | """ 29 | slug = self.cleaned_data["slug"] 30 | if slug == "create": 31 | raise ValidationError( 32 | "Slug may not be 'create'." 33 | ) 34 | return slug 35 | 36 | 37 | class TagForm(LowercaseNameMixin, ModelForm): 38 | """HTML form for Tag objects""" 39 | 40 | class Meta: 41 | model = Tag 42 | fields = "__all__" # name only, no slug! 43 | 44 | 45 | class StartupForm( 46 | LowercaseNameMixin, SlugCleanMixin, ModelForm 47 | ): 48 | """HTML form for Startup objects""" 49 | 50 | class Meta: 51 | model = Startup 52 | fields = "__all__" 53 | 54 | 55 | class NewsLinkForm(ModelForm): 56 | """HTML form for NewsLink objects""" 57 | 58 | class Meta: 59 | model = NewsLink 60 | fields = "__all__" 61 | widgets = {"startup": HiddenInput()} 62 | 63 | def clean_slug(self): 64 | """Avoid URI conflicts with paths in app""" 65 | slug = self.cleaned_data["slug"] 66 | if slug in ["delete", "update", "add_article"]: 67 | raise ValidationError( 68 | f"Slug may not be '{slug}'." 69 | ) 70 | return slug 71 | -------------------------------------------------------------------------------- /src/organizer/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | import django.db.models.deletion 2 | from django.db import migrations, models 3 | from django_extensions.db.fields import AutoSlugField 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="Tag", 15 | fields=[ 16 | ( 17 | "id", 18 | models.AutoField( 19 | auto_created=True, 20 | primary_key=True, 21 | serialize=False, 22 | verbose_name="ID", 23 | ), 24 | ), 25 | ( 26 | "name", 27 | models.CharField( 28 | max_length=31, unique=True 29 | ), 30 | ), 31 | ( 32 | "slug", 33 | AutoSlugField( 34 | blank=True, 35 | editable=False, 36 | help_text="A label for URL config.", 37 | max_length=31, 38 | populate_from=["name"], 39 | ), 40 | ), 41 | ], 42 | options={"ordering": ["name"]}, 43 | ), 44 | migrations.CreateModel( 45 | name="Startup", 46 | fields=[ 47 | ( 48 | "id", 49 | models.AutoField( 50 | auto_created=True, 51 | primary_key=True, 52 | serialize=False, 53 | verbose_name="ID", 54 | ), 55 | ), 56 | ( 57 | "name", 58 | models.CharField( 59 | db_index=True, max_length=31 60 | ), 61 | ), 62 | ( 63 | "slug", 64 | models.SlugField( 65 | help_text="A label for URL config.", 66 | max_length=31, 67 | unique=True, 68 | ), 69 | ), 70 | ("description", models.TextField()), 71 | ( 72 | "founded_date", 73 | models.DateField( 74 | verbose_name="date founded" 75 | ), 76 | ), 77 | ( 78 | "contact", 79 | models.EmailField(max_length=254), 80 | ), 81 | ( 82 | "website", 83 | models.URLField(max_length=255), 84 | ), 85 | ( 86 | "tags", 87 | models.ManyToManyField( 88 | to="organizer.Tag" 89 | ), 90 | ), 91 | ], 92 | options={ 93 | "ordering": ["name"], 94 | "get_latest_by": "founded_date", 95 | }, 96 | ), 97 | migrations.CreateModel( 98 | name="NewsLink", 99 | fields=[ 100 | ( 101 | "id", 102 | models.AutoField( 103 | auto_created=True, 104 | primary_key=True, 105 | serialize=False, 106 | verbose_name="ID", 107 | ), 108 | ), 109 | ("title", models.CharField(max_length=63)), 110 | ("slug", models.SlugField(max_length=63)), 111 | ( 112 | "pub_date", 113 | models.DateField( 114 | verbose_name="date published" 115 | ), 116 | ), 117 | ("link", models.URLField(max_length=255)), 118 | ( 119 | "startup", 120 | models.ForeignKey( 121 | on_delete=django.db.models.deletion.CASCADE, 122 | to="organizer.Startup", 123 | ), 124 | ), 125 | ], 126 | options={ 127 | "verbose_name": "news article", 128 | "ordering": ["-pub_date"], 129 | "get_latest_by": "pub_date", 130 | }, 131 | ), 132 | migrations.AlterUniqueTogether( 133 | name="newslink", 134 | unique_together={("slug", "startup")}, 135 | ), 136 | ] 137 | -------------------------------------------------------------------------------- /src/organizer/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jambonrose/python-web-dev-21-2/ab8bd1e8a394e5dca1f14b821dc5344594c172f4/src/organizer/migrations/__init__.py -------------------------------------------------------------------------------- /src/organizer/models.py: -------------------------------------------------------------------------------- 1 | """Django data models for organizing startup company data 2 | 3 | Django Model Documentation: 4 | https://docs.djangoproject.com/en/2.1/topics/db/models/ 5 | https://docs.djangoproject.com/en/2.1/ref/models/options/ 6 | https://docs.djangoproject.com/en/2.1/internals/contributing/writing-code/coding-style/#model-style 7 | Django Field Reference: 8 | https://docs.djangoproject.com/en/2.1/ref/models/fields/ 9 | https://docs.djangoproject.com/en/2.1/ref/models/fields/#charfield 10 | https://docs.djangoproject.com/en/2.1/ref/models/fields/#datefield 11 | https://docs.djangoproject.com/en/2.1/ref/models/fields/#emailfield 12 | https://docs.djangoproject.com/en/2.1/ref/models/fields/#foreignkey 13 | https://docs.djangoproject.com/en/2.1/ref/models/fields/#manytomanyfield 14 | https://docs.djangoproject.com/en/2.1/ref/models/fields/#slugfield 15 | https://docs.djangoproject.com/en/2.1/ref/models/fields/#textfield 16 | https://docs.djangoproject.com/en/2.1/ref/models/fields/#urlfield 17 | 18 | AutoSlugField Reference: 19 | https://django-extensions.readthedocs.io/en/latest/field_extensions.html 20 | 21 | """ 22 | from django.db.models import ( 23 | CASCADE, 24 | CharField, 25 | DateField, 26 | EmailField, 27 | ForeignKey, 28 | ManyToManyField, 29 | Model, 30 | SlugField, 31 | TextField, 32 | URLField, 33 | ) 34 | from django.urls import reverse 35 | from django_extensions.db.fields import AutoSlugField 36 | 37 | 38 | class Tag(Model): 39 | """Labels to help categorize data""" 40 | 41 | name = CharField(max_length=31, unique=True) 42 | slug = AutoSlugField( 43 | help_text="A label for URL config.", 44 | max_length=31, 45 | populate_from=["name"], 46 | ) 47 | 48 | class Meta: 49 | ordering = ["name"] 50 | 51 | def __str__(self): 52 | return self.name 53 | 54 | def get_absolute_url(self): 55 | """Return URL to detail page of Tag""" 56 | return reverse( 57 | "tag_detail", kwargs={"slug": self.slug} 58 | ) 59 | 60 | def get_update_url(self): 61 | """Return URL to update page of Tag""" 62 | return reverse( 63 | "tag_update", kwargs={"slug": self.slug} 64 | ) 65 | 66 | def get_delete_url(self): 67 | """Return URL to delete page of Tag""" 68 | return reverse( 69 | "tag_delete", kwargs={"slug": self.slug} 70 | ) 71 | 72 | 73 | class Startup(Model): 74 | """Data about a Startup company""" 75 | 76 | name = CharField(max_length=31, db_index=True) 77 | slug = SlugField( 78 | max_length=31, 79 | unique=True, 80 | help_text="A label for URL config.", 81 | ) 82 | description = TextField() 83 | founded_date = DateField("date founded") 84 | contact = EmailField() 85 | website = URLField( 86 | max_length=255 # https://tools.ietf.org/html/rfc3986 87 | ) 88 | tags = ManyToManyField(Tag) 89 | 90 | class Meta: 91 | get_latest_by = "founded_date" 92 | ordering = ["name"] 93 | 94 | def __str__(self): 95 | return self.name 96 | 97 | def get_absolute_url(self): 98 | """Return URL to detail page of Startup""" 99 | return reverse( 100 | "startup_detail", kwargs={"slug": self.slug} 101 | ) 102 | 103 | def get_update_url(self): 104 | """Return URL to update page of Startup""" 105 | return reverse( 106 | "startup_update", kwargs={"slug": self.slug} 107 | ) 108 | 109 | def get_delete_url(self): 110 | """Return URL to delete page of Startup""" 111 | return reverse( 112 | "startup_delete", kwargs={"slug": self.slug} 113 | ) 114 | 115 | def get_newslink_create_url(self): 116 | """Return URL to detail page of Startup""" 117 | return reverse( 118 | "newslink_create", 119 | kwargs={"startup_slug": self.slug}, 120 | ) 121 | 122 | 123 | class NewsLink(Model): 124 | """Link to external sources about a Startup""" 125 | 126 | title = CharField(max_length=63) 127 | slug = SlugField(max_length=63) 128 | pub_date = DateField("date published") 129 | link = URLField( 130 | max_length=255 # https://tools.ietf.org/html/rfc3986 131 | ) 132 | startup = ForeignKey(Startup, on_delete=CASCADE) 133 | 134 | class Meta: 135 | get_latest_by = "pub_date" 136 | ordering = ["-pub_date"] 137 | unique_together = ("slug", "startup") 138 | verbose_name = "news article" 139 | 140 | def __str__(self): 141 | return f"{self.startup}: {self.title}" 142 | 143 | def get_absolute_url(self): 144 | """Return URL to detail page of Startup""" 145 | return reverse( 146 | "startup_detail", 147 | kwargs={"slug": self.startup.slug}, 148 | ) 149 | 150 | def get_update_url(self): 151 | """Return URL to update page of Startup""" 152 | return reverse( 153 | "newslink_update", 154 | kwargs={ 155 | "startup_slug": self.startup.slug, 156 | "newslink_slug": self.slug, 157 | }, 158 | ) 159 | 160 | def get_delete_url(self): 161 | """Return URL to delete page of Startup""" 162 | return reverse( 163 | "newslink_delete", 164 | kwargs={ 165 | "startup_slug": self.startup.slug, 166 | "newslink_slug": self.slug, 167 | }, 168 | ) 169 | -------------------------------------------------------------------------------- /src/organizer/routers.py: -------------------------------------------------------------------------------- 1 | """URL Paths and Routers for Organizer App""" 2 | from rest_framework.routers import SimpleRouter 3 | 4 | from .viewsets import ( 5 | NewsLinkViewSet, 6 | StartupViewSet, 7 | TagViewSet, 8 | ) 9 | 10 | 11 | class NewsLinkRouter(SimpleRouter): 12 | """Override the SimpleRouter for articles 13 | 14 | DRF's routers expect there to only be a single variable 15 | for finding objects. However, our NewsLinks needs 16 | two! We therefore override the Router's behavior to 17 | make it do what we want. 18 | 19 | The big question: was it worth switching to a ViewSet 20 | and Router over our previous config for this? 21 | """ 22 | 23 | def get_lookup_regex(self, *args, **kwargs): 24 | """Return regular expression pattern for URL path 25 | 26 | This is the (rough) equivalent of the simple path: 27 | / 28 | """ 29 | return ( 30 | r"(?P[^/.]+)/" 31 | r"(?P[^/.]+)" 32 | ) 33 | 34 | 35 | api_router = SimpleRouter() 36 | api_router.register("tag", TagViewSet, base_name="api-tag") 37 | api_router.register( 38 | "startup", StartupViewSet, base_name="api-startup" 39 | ) 40 | 41 | nl_router = NewsLinkRouter() 42 | nl_router.register( 43 | "newslink", NewsLinkViewSet, base_name="api-newslink" 44 | ) 45 | 46 | urlpatterns = api_router.urls + nl_router.urls 47 | -------------------------------------------------------------------------------- /src/organizer/serializers.py: -------------------------------------------------------------------------------- 1 | """Serializers for the Organizer App 2 | 3 | Serializer Documentation 4 | http://www.django-rest-framework.org/api-guide/serializers/ 5 | http://www.django-rest-framework.org/api-guide/fields/ 6 | http://www.django-rest-framework.org/api-guide/relations/ 7 | """ 8 | from rest_framework.reverse import reverse 9 | from rest_framework.serializers import ( 10 | HyperlinkedModelSerializer, 11 | HyperlinkedRelatedField, 12 | ModelSerializer, 13 | SerializerMethodField, 14 | ) 15 | 16 | from .models import NewsLink, Startup, Tag 17 | 18 | 19 | class TagSerializer(HyperlinkedModelSerializer): 20 | """Serialize Tag data""" 21 | 22 | class Meta: 23 | model = Tag 24 | fields = "__all__" 25 | extra_kwargs = { 26 | "url": { 27 | "lookup_field": "slug", 28 | "view_name": "api-tag-detail", 29 | } 30 | } 31 | 32 | 33 | class StartupSerializer(HyperlinkedModelSerializer): 34 | """Serialize Startup data""" 35 | 36 | tags = HyperlinkedRelatedField( 37 | lookup_field="slug", 38 | many=True, 39 | read_only=True, 40 | view_name="api-tag-detail", 41 | ) 42 | 43 | class Meta: 44 | model = Startup 45 | fields = "__all__" 46 | extra_kwargs = { 47 | "url": { 48 | "lookup_field": "slug", 49 | "view_name": "api-startup-detail", 50 | } 51 | } 52 | 53 | 54 | class NewsLinkSerializer(ModelSerializer): 55 | """Serialize NewsLink data""" 56 | 57 | url = SerializerMethodField() 58 | startup = HyperlinkedRelatedField( 59 | queryset=Startup.objects.all(), 60 | lookup_field="slug", 61 | view_name="api-startup-detail", 62 | ) 63 | 64 | class Meta: 65 | model = NewsLink 66 | exclude = ("id",) 67 | 68 | def get_url(self, newslink): 69 | """Build full URL for NewsLink API detail""" 70 | return reverse( 71 | "api-newslink-detail", 72 | kwargs=dict( 73 | startup_slug=newslink.startup.slug, 74 | newslink_slug=newslink.slug, 75 | ), 76 | request=self.context["request"], 77 | ) 78 | -------------------------------------------------------------------------------- /src/organizer/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /src/organizer/urls.py: -------------------------------------------------------------------------------- 1 | """URL paths for Organizer App""" 2 | from django.urls import path 3 | 4 | from .views import ( 5 | NewsLinkCreate, 6 | NewsLinkDelete, 7 | NewsLinkDetail, 8 | NewsLinkUpdate, 9 | StartupCreate, 10 | StartupDelete, 11 | StartupDetail, 12 | StartupList, 13 | StartupUpdate, 14 | TagCreate, 15 | TagDelete, 16 | TagDetail, 17 | TagList, 18 | TagUpdate, 19 | ) 20 | 21 | urlpatterns = [ 22 | path( 23 | "startup/", 24 | StartupList.as_view(), 25 | name="startup_list", 26 | ), 27 | path( 28 | "startup/create/", 29 | StartupCreate.as_view(), 30 | name="startup_create", 31 | ), 32 | path( 33 | "startup//", 34 | StartupDetail.as_view(), 35 | name="startup_detail", 36 | ), 37 | path( 38 | "startup//delete/", 39 | StartupDelete.as_view(), 40 | name="startup_delete", 41 | ), 42 | path( 43 | "startup//update/", 44 | StartupUpdate.as_view(), 45 | name="startup_update", 46 | ), 47 | path( 48 | "startup//add_article/", 49 | NewsLinkCreate.as_view(), 50 | name="newslink_create", 51 | ), 52 | path( 53 | "startup///", 54 | NewsLinkDetail.as_view(), 55 | name="newslink_detail", 56 | ), 57 | path( 58 | "startup///" 59 | "delete/", 60 | NewsLinkDelete.as_view(), 61 | name="newslink_delete", 62 | ), 63 | path( 64 | "startup///" 65 | "update/", 66 | NewsLinkUpdate.as_view(), 67 | name="newslink_update", 68 | ), 69 | path("tag/", TagList.as_view(), name="tag_list"), 70 | path( 71 | "tag/create/", 72 | TagCreate.as_view(), 73 | name="tag_create", 74 | ), 75 | path( 76 | "tag//", 77 | TagDetail.as_view(), 78 | name="tag_detail", 79 | ), 80 | path( 81 | "tag//update/", 82 | TagUpdate.as_view(), 83 | name="tag_update", 84 | ), 85 | path( 86 | "tag//delete/", 87 | TagDelete.as_view(), 88 | name="tag_delete", 89 | ), 90 | ] 91 | -------------------------------------------------------------------------------- /src/organizer/view_mixins.py: -------------------------------------------------------------------------------- 1 | """Mix-in classes for Organizer Views""" 2 | from django.core.exceptions import SuspiciousOperation 3 | from django.shortcuts import get_object_or_404 4 | 5 | from .models import NewsLink, Startup 6 | 7 | 8 | class NewsLinkContextMixin: 9 | """Add Startup to template context in NewsLink views""" 10 | 11 | def get_context_data(self, **kwargs): 12 | """Dynamically add to template context 13 | 14 | http://ccbv.co.uk/ContextMixin 15 | """ 16 | startup = get_object_or_404( 17 | Startup, slug=self.kwargs.get("startup_slug") 18 | ) 19 | return super().get_context_data( 20 | startup=startup, **kwargs 21 | ) 22 | 23 | 24 | class NewsLinkObjectMixin: 25 | """Django View mix-in to find NewsLinks""" 26 | 27 | model = NewsLink 28 | 29 | def get_object(self, queryset=None): 30 | """Get NewsLink from database 31 | 32 | http://ccbv.co.uk/SingleObjectMixin 33 | """ 34 | if queryset is None: 35 | if hasattr(self, "get_queryset"): 36 | queryset = self.get_queryset() 37 | else: 38 | queryset = self.model.objects.all() 39 | 40 | # Django's View class puts URI kwargs in dictionary 41 | startup_slug = self.kwargs.get("startup_slug") 42 | newslink_slug = self.kwargs.get("newslink_slug") 43 | 44 | if startup_slug is None or newslink_slug is None: 45 | raise AttributeError( 46 | f"View {self.__class__.__name__} must be" 47 | f"called with a slug for a Startup and a" 48 | f"slug for a NewsLink objects." 49 | ) 50 | 51 | return get_object_or_404( 52 | queryset, 53 | startup__slug=startup_slug, 54 | slug=newslink_slug, 55 | ) 56 | 57 | 58 | class VerifyStartupFkToUriMixin: 59 | """Mixin to verify Startup data in NewsLink views 60 | 61 | NewsLink views to create and update specify the Startup 62 | slug in the URI. However, for simplicity when 63 | interacting with the NewsLinkForm, the form also has a 64 | field for the Startup object. This class ensures that 65 | the Startup referred to by the URI and by the 66 | NewsLinkForm field is one and the same. 67 | """ 68 | 69 | def verify_startup_fk_matches_uri(self): 70 | """Raise HTTP 400 if Startup data mismatched""" 71 | startup = get_object_or_404( 72 | Startup, slug=self.kwargs.get("startup_slug") 73 | ) 74 | form_startup_pk = self.request.POST.get("startup") 75 | if str(startup.pk) != form_startup_pk: 76 | raise SuspiciousOperation( 77 | "Startup Form PK and URI do not match" 78 | ) 79 | 80 | def post(self, request, *args, **kwargs): 81 | """Check Startup data before form submission process 82 | 83 | - Raise HTTP 400 if Startup data mismatched 84 | - Hook into Generic Views for rest of work 85 | """ 86 | self.verify_startup_fk_matches_uri() 87 | return super().post(request, *args, **kwargs) 88 | -------------------------------------------------------------------------------- /src/organizer/views.py: -------------------------------------------------------------------------------- 1 | """Views for Organizer App""" 2 | from django.contrib.auth.mixins import LoginRequiredMixin 3 | from django.shortcuts import get_object_or_404 4 | from django.urls import reverse_lazy 5 | from django.views.generic import ( 6 | CreateView, 7 | DeleteView, 8 | DetailView, 9 | ListView, 10 | RedirectView, 11 | UpdateView, 12 | ) 13 | 14 | from .forms import NewsLinkForm, StartupForm, TagForm 15 | from .models import NewsLink, Startup, Tag 16 | from .view_mixins import ( 17 | NewsLinkContextMixin, 18 | NewsLinkObjectMixin, 19 | VerifyStartupFkToUriMixin, 20 | ) 21 | 22 | 23 | class NewsLinkCreate( 24 | VerifyStartupFkToUriMixin, 25 | NewsLinkContextMixin, 26 | LoginRequiredMixin, 27 | CreateView, 28 | ): 29 | """Create a link to an article about a startup""" 30 | 31 | extra_context = {"update": False} 32 | form_class = NewsLinkForm 33 | model = NewsLink 34 | template_name = "newslink/form.html" 35 | 36 | def get_initial(self): 37 | """Pre-select Startup in NewsLinkForm""" 38 | startup = get_object_or_404( 39 | Startup, slug=self.kwargs.get("startup_slug") 40 | ) 41 | return dict( 42 | super().get_initial(), startup=startup.pk 43 | ) 44 | 45 | 46 | class NewsLinkDelete( 47 | NewsLinkObjectMixin, 48 | NewsLinkContextMixin, 49 | LoginRequiredMixin, 50 | DeleteView, 51 | ): 52 | """Delete a link to an article about a startup""" 53 | 54 | template_name = "newslink/confirm_delete.html" 55 | 56 | def get_success_url(self): 57 | """Return the detail page of the Startup parent 58 | 59 | http://ccbv.co.uk/DeletionMixin 60 | """ 61 | startup = get_object_or_404( 62 | Startup, slug=self.kwargs.get("startup_slug") 63 | ) 64 | return startup.get_absolute_url() 65 | 66 | 67 | class NewsLinkDetail(NewsLinkObjectMixin, RedirectView): 68 | """Redirect to Startup Detail page 69 | 70 | http://ccbv.co.uk/RedirectView/ 71 | """ 72 | 73 | def get_redirect_url(self, *args, **kwargs): 74 | """Redirect user to Startup page""" 75 | return self.get_object().get_absolute_url() 76 | 77 | 78 | class NewsLinkUpdate( 79 | VerifyStartupFkToUriMixin, 80 | NewsLinkObjectMixin, 81 | NewsLinkContextMixin, 82 | LoginRequiredMixin, 83 | UpdateView, 84 | ): 85 | """Update a link to an article about a startup""" 86 | 87 | extra_context = {"update": True} 88 | form_class = NewsLinkForm 89 | template_name = "newslink/form.html" 90 | 91 | 92 | class TagList(ListView): 93 | """Display a list of Tags""" 94 | 95 | queryset = Tag.objects.all() 96 | template_name = "tag/list.html" 97 | 98 | 99 | class TagDetail(DetailView): 100 | """Display a single Tag""" 101 | 102 | queryset = Tag.objects.all() 103 | template_name = "tag/detail.html" 104 | 105 | 106 | class TagCreate(LoginRequiredMixin, CreateView): 107 | """Create new Tags via HTML form""" 108 | 109 | form_class = TagForm 110 | model = Tag 111 | template_name = "tag/form.html" 112 | extra_context = {"update": False} 113 | 114 | 115 | class TagUpdate(LoginRequiredMixin, UpdateView): 116 | """Update a Tag via HTML form""" 117 | 118 | form_class = TagForm 119 | model = Tag 120 | template_name = "tag/form.html" 121 | extra_context = {"update": True} 122 | 123 | 124 | class TagDelete(LoginRequiredMixin, DeleteView): 125 | """Confirm and delete a Tag via HTML Form""" 126 | 127 | model = Tag 128 | template_name = "tag/confirm_delete.html" 129 | success_url = reverse_lazy("tag_list") 130 | 131 | 132 | class StartupCreate(LoginRequiredMixin, CreateView): 133 | """Create new Startups via HTML form""" 134 | 135 | form_class = StartupForm 136 | model = Startup 137 | template_name = "startup/form.html" 138 | extra_context = {"update": False} 139 | 140 | 141 | class StartupDelete(LoginRequiredMixin, DeleteView): 142 | """Confirm and delete a Startup via HTML Form""" 143 | 144 | model = Startup 145 | template_name = "startup/confirm_delete.html" 146 | success_url = reverse_lazy("startup_list") 147 | 148 | 149 | class StartupList(ListView): 150 | """Display a list of Startups""" 151 | 152 | queryset = Startup.objects.all() 153 | template_name = "startup/list.html" 154 | 155 | 156 | class StartupDetail(DetailView): 157 | """Display a single Startup""" 158 | 159 | queryset = Startup.objects.all() 160 | template_name = "startup/detail.html" 161 | 162 | 163 | class StartupUpdate(LoginRequiredMixin, UpdateView): 164 | """Update a Startup via HTML form""" 165 | 166 | form_class = StartupForm 167 | model = Startup 168 | template_name = "startup/form.html" 169 | extra_context = {"update": True} 170 | -------------------------------------------------------------------------------- /src/organizer/viewsets.py: -------------------------------------------------------------------------------- 1 | """Viewsets for the Organizer App""" 2 | from django.shortcuts import get_object_or_404 3 | from rest_framework.decorators import action 4 | from rest_framework.response import Response 5 | from rest_framework.status import ( 6 | HTTP_204_NO_CONTENT, 7 | HTTP_400_BAD_REQUEST, 8 | ) 9 | from rest_framework.viewsets import ModelViewSet 10 | 11 | from .models import NewsLink, Startup, Tag 12 | from .serializers import ( 13 | NewsLinkSerializer, 14 | StartupSerializer, 15 | TagSerializer, 16 | ) 17 | 18 | 19 | class TagViewSet(ModelViewSet): 20 | """A set of views for the Tag model""" 21 | 22 | lookup_field = "slug" 23 | queryset = Tag.objects.all() 24 | serializer_class = TagSerializer 25 | 26 | 27 | class StartupViewSet(ModelViewSet): 28 | """A set of views for the Startup model""" 29 | 30 | lookup_field = "slug" 31 | queryset = Startup.objects.all() 32 | serializer_class = StartupSerializer 33 | 34 | @action(detail=True, methods=["HEAD", "GET", "POST"]) 35 | def tags(self, request, slug=None): 36 | """Relate a POSTed Tag to Startup in URI""" 37 | startup = self.get_object() 38 | if request.method in ("HEAD", "GET"): 39 | s_tag = TagSerializer( 40 | startup.tags, 41 | many=True, 42 | context={"request": request}, 43 | ) 44 | return Response(s_tag.data) 45 | tag_slug = request.data.get("slug") 46 | if not tag_slug: 47 | return Response( 48 | "Slug of Tag must be specified", 49 | status=HTTP_400_BAD_REQUEST, 50 | ) 51 | tag = get_object_or_404(Tag, slug__iexact=tag_slug) 52 | startup.tags.add(tag) 53 | return Response(status=HTTP_204_NO_CONTENT) 54 | 55 | 56 | class NewsLinkViewSet(ModelViewSet): 57 | """A set of views for the Startup model""" 58 | 59 | queryset = NewsLink.objects.all() 60 | serializer_class = NewsLinkSerializer 61 | 62 | def get_object(self): 63 | """Override DRF's generic method 64 | 65 | http://www.cdrf.co/3.7/rest_framework.viewsets/ModelViewSet.html#get_object 66 | """ 67 | startup_slug = self.kwargs.get("startup_slug") 68 | newslink_slug = self.kwargs.get("newslink_slug") 69 | 70 | queryset = self.filter_queryset(self.get_queryset()) 71 | 72 | newslink = get_object_or_404( 73 | queryset, 74 | slug=newslink_slug, 75 | startup__slug=startup_slug, 76 | ) 77 | self.check_object_permissions( 78 | self.request, newslink 79 | ) 80 | return newslink 81 | -------------------------------------------------------------------------------- /src/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block title %}Startup Organizer{% endblock %} 7 | 8 | 9 | 10 | 17 | 25 | {% block content %}{% endblock %} 26 |