├── .gitignore ├── README.md └── guides ├── day1.md ├── day2.md ├── day3.md ├── day4.md └── trouble.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Intro to Django 2 | 3 | Fork this repo to use for your projects this week. 4 | 5 | ## Reading 6 | 7 | * [Read Me First: Common Errors, Gotchas, and Troubleshooting](guides/trouble.md) 8 | 9 | * [Day 1: Intro](guides/day1.md) 10 | * [Day 2: Admin Interface and SQL](guides/day2.md) 11 | * [Day 3: Setting up a RESTful API](guides/day3.md) 12 | * [Day 4: Token Auth for REST](guides/day4.md) 13 | 14 | ### External links 15 | 16 | * [Virtual Environments Primer](https://realpython.com/python-virtual-environments-a-primer/) 17 | 18 | ## Deliverables 19 | 20 | * Implement an app similar to the `notes` app, but with data of your choosing. 21 | * Submit a file `models.txt` that describes (to other developers) what data you are storing in the database. 22 | * The app needs to support authentication and expose a RESTful API to the data. 23 | 24 | ## Additional Deliverables 25 | 26 | (In arbitrary order.) 27 | 28 | * Add filters and search capabilities to your REST API. 29 | * Add ability to upload attachments to your records; e.g. images, etc. 30 | * Add ability to change an existing record, not just GET and POST. 31 | * Add a Django front end to the data. 32 | * Brainstorm a list of 10 additional features users would find useful. 33 | * Implement the brainstormed list. 34 | -------------------------------------------------------------------------------- /guides/day1.md: -------------------------------------------------------------------------------- 1 | # Day 1: Intro 2 | 3 | ## Summary 4 | 5 | * Get pipenv installed 6 | * Clone your repo 7 | * (If you cloned the Hello-Django repo, delete the file `requirements.txt`!) 8 | * Go to your repo root directory 9 | * `pipenv --three` 10 | * `pipenv install` 11 | * `pipenv shell` 12 | * `pipenv install django` 13 | * `django-admin startproject djorg .` 14 | * `django-admin startapp notes` 15 | * `./manage.py runserver` 16 | * `./manage.py showmigrations` 17 | * `./manage.py migrate` 18 | * `./manage.py runserver` 19 | * Add model to `notes/models.py` 20 | * Add `'notes'` to `INSTALLED_APPS` in `djorg/settings.py` 21 | * `./manage.py showmigrations` 22 | * `./manage.py makemigrations` 23 | * `./manage.py showmigrations` 24 | * `./manage.py migrate` 25 | * `./manage.py shell` 26 | * `from notes.models import Note` 27 | * `n = Note(title="example", content="This is a test.")` 28 | * `n.save()` 29 | * `exit()` 30 | * `./manage.py shell` 31 | * `from notes.models import Note` 32 | * `x = Note.objects.all()` 33 | * `x[0]` 34 | * `x[0].content` 35 | * `exit()` 36 | * `pipenv install python-decouple` 37 | * Add config information to `settings.py` and `.env` 38 | 39 | 40 | ## Setting up a Virtual Environment 41 | 42 | Check Python version and install or upgrade if less than 3.5.x. 43 | 44 | Check Pip version and install or upgrade if less than 10.x. 45 | 46 | * Mac/Linux - Option A: `sudo -H pip3 install --upgrade pip` 47 | * Mac/Linux - Option B: `brew upgrade python` 48 | * PC - `python -m pip install --upgrade pip` 49 | 50 | 51 | Check Pipenv version and install or upgrade if less than 2018.x 52 |
*(2018.10.13 as of November 6, 2018)* 53 | 54 | 55 | Normally you'd make a repo with a README and a Python gitignore, but since we're 56 | going to be using pull requests to turn things in, just fork this repo instead. 57 | 58 | 59 | Clone repo on to local machine. 60 | 61 | In the terminal, navigate to root folder of repo. 62 | 63 | Create pipenv virtual environment: 64 | 65 | ``` 66 | pipenv --three 67 | ``` 68 | 69 | * The `--three` option tells it to use Python3 70 | * This is similar to using `npm`/`yarn` 71 | 72 | 73 | Verify that the `Pipfile` was created in the root of the repo. 74 | 75 | 76 | Activate pipenv with `pipenv shell` 77 | 78 | * You should see the command line change to the name of your repo/folder 79 | followed by a dash and a random string. 80 | * We are using pipenv because it is newer and more robust. Uses a lockfile 81 | similar to npm/yarn. Easier to get into and out of shell. 82 | * To get back in, use `pipenv shell` from the root directory of the project. 83 | 84 | ## To Start a Django Project and App 85 | 86 | Once you are in the virtual environment, install django: 87 | 88 | ``` 89 | pipenv install django 90 | ``` 91 | 92 | * We are using a virtual environment instead of installing globally because 93 | installing globally would be like using npm/yarn install globally and 94 | installing all the packages on everything. 95 | 96 | Add `Pipfile` and `Pipfile.lock` to the repo with `git add Pipfile*` and commit 97 | with `git commit -m "added pipfiles"`. 98 | 99 | Start a project with `django-admin startproject [name_of_project] .` 100 | 101 | * Replace [nameofproject] with the name of your project 102 | * The . tells it to create the project in the current directory. Otherwise, it 103 | would create a project in a subdirectory called [name_of_project]. We don’t 104 | need that because we want the repo folder to be the root 105 | 106 | Verify that the [name_of_project] folder was created and has boilerplate files 107 | such as `__init__.py`. 108 | 109 | The project is what it was named above. A project is made up of a collection of 110 | apps. It can have one or many. 111 | 112 | 113 | Create an app with `django-admin startapp [name_of_app]` 114 | 115 | * For the first project, we are naming the app notes 116 | * Name it differently as appropriate if you are following this to set up, but 117 | working on something else. 118 | 119 | ## Start the Server 120 | 121 | Verify that the [name_of_app] subdirectory has been created 122 | 123 | Test by navigating to the project folder root/[name_of_project] and running 124 | `./manage.py runserver` 125 | 126 | * This should launch the animated rocket default page 127 | * Take note of the warning about unapplied migrations. We will fix that in a moment 128 | 129 | Django makes it easier to make changes to databases. This is called migration(s). 130 | 131 | 132 | ## Migrations 133 | 134 | Run `./manage.py showmigrations`. This will show a list of outstanding 135 | changes that need to occur. 136 | 137 | To take a closer look at what is being done, you can look at the SQL queries 138 | that Django is building. _This step is entirely optional, and is only for the 139 | curious--which should be you!_ 140 | 141 | ``` 142 | ./manage.py sqlmigrate [package_name] [migration_id] 143 | ``` 144 | 145 | for example 146 | 147 | ``` 148 | ./manage.py sqlmigrate admin 0001_initial 149 | ``` 150 | 151 | * This will display a large number of sql commands that may not make sense if 152 | you are not yet familiar with SQL. 153 | * This doesn’t actually do anything. It just displays info. 154 | * These are all the data structures that your python code has created. Django 155 | turns this into sql tables, etc. for you. (If you’ve ever done this manually, 156 | you know how awesome that is :) ) 157 | 158 | 159 | To actually run the migrations, use: 160 | 161 | ``` 162 | ./manage.py migrate 163 | ``` 164 | 165 | Check them by showing migrations again: `./manage.py showmigrations` 166 | 167 | * The list of migrations should show an `x` for each item. 168 | 169 | Run the server again and confirm that the migration warning is not present. 170 | There won’t be a change to the actual page that renders. 171 | 172 | ## Adding Data Models 173 | 174 | In the `notes` folder, open `models.py`. 175 | 176 | Create a class called `notes` that inherits from `models.Model`: 177 | 178 | ```python 179 | class Note(models.Model): 180 | ``` 181 | 182 | This gives our new class access to all of the built-in functionality in `models.Model`. 183 | 184 | Think about the data that we need for standard web notes functionality. We might 185 | want a title, body, some timestamps, etc. We can use the docs to find the types 186 | of things we can add: 187 | 188 | https://docs.djangoproject.com/en/2.0/ref/models/ 189 | 190 | Add the following variables to the class: 191 | 192 | ```python 193 | class Note(models.Model): 194 | title = models.CharField(max_length=200) 195 | content = models.TextField(blank=True) 196 | ``` 197 | 198 | Add any additional fields you would like as well. Check out the [Django docs](https://docs.djangoproject.com/es/2.1/ref/models/fields/) for more details on what fields the Model class makes available to us. 199 | 200 | We also need something to serve as a unique identifier for each record. We’ll 201 | use something called a UUID for this: 202 | [https://en.wikipedia.org/wiki/Universally_unique_identifier](https://en.wikipedia.org/wiki/Universally_unique_identifier). 203 | 204 | Add a UUID to serve as a key for each record. 205 | 206 | * First, import the library: 207 | ```python 208 | from uuid import uuid4 209 | ``` 210 | * Second, add the field: 211 | ```python 212 | id = models.UUIDField(primary_key=True, default=uuid4, editable=False) 213 | ``` 214 | 215 | * Primary key is how the database tracks records. 216 | * Default calls a function to randomly generate a unique identifier. 217 | * We make editable false because we never want to change the key. 218 | * Put it at the top of the list of fields because it’s sort of like the index 219 | for the record. 220 | 221 | Next, we need to tell the project that the app exists. Open `settings.py` from 222 | the project folder. 223 | 224 | Find the section for `INSTALLED_APPS` and add `'notes'`, or other apps as 225 | appropriate. 226 | 227 | In the console, check for migrations again with `./manage.py 228 | showmigrations`. The notes app should show up in the list now, but it has no 229 | migrations. 230 | 231 | To generate the migrations, run: 232 | 233 | ``` 234 | ./manage.py makemigrations 235 | ``` 236 | 237 | If you get an error that there are no changes to make, double-check that you 238 | have saved `models.py`. 239 | 240 | Show migrations again to make sure they appear, then do the migration: 241 | ``` 242 | ./manage.py migrate 243 | ``` 244 | 245 | ## Adding Data with the Python Shell 246 | 247 | `manage.py` has its own shell. Run `./manage.py shell` to bring up a Python repl. 248 | 249 | * The input line should change to `>>>` 250 | 251 | Import the notes class into the repl: 252 | ```python 253 | from notes.models import Note 254 | ``` 255 | 256 | Create a new note with: 257 | ```python 258 | n = Note(title="example", content="This is a test.") 259 | ``` 260 | 261 | Check by the name of your variable to make sure worked: 262 | ```python 263 | n 264 | ``` 265 | 266 | We can use a built in function from models (which `Note` inherited from!) to 267 | save this to the database: 268 | ```python 269 | n.save() 270 | ``` 271 | 272 | Exit the terminal, then restart it - `exit()` then `./manage.py shell` 273 | 274 | We have to import the `Note` class again, using the same command as before. 275 | 276 | There is another built in method that will retrieve all existing objects of a 277 | class: `Note.objects.all()` 278 | 279 | Use this to save the data back into a variable named `b` and explore. 280 | 281 | ## Moving the Secret Key to `.env` 282 | 283 | Take a look at that secret key in `settings.py`. 284 | 285 | We want to move this out of the settings file so that it doesn’t get checked 286 | into source control. We’ll move it to another file, which everyone on the 287 | project will need a copy of, but it won’t be in the repo itself. 288 | 289 | We’re going to make use of a module called Python Decouple by installing it in 290 | the virtual environment: 291 | ``` 292 | pipenv install python-decouple 293 | ``` 294 | 295 | Once it’s installed, we can bring it into `settings.py` with: 296 | ```python 297 | from decouple import config 298 | ``` 299 | 300 | Pull up the docs for Python Decouple and take a look at the usage and rationale. 301 | There is an example for how to use it with Django. Follow that to remove the key 302 | from settings. 303 | 304 | Add the key your `.env` file (creating it if you have to): 305 | ``` 306 | SECRET_KEY='...whatever it was in the settings file...' 307 | ``` 308 | 309 | Then change the line in `settings.py` to: 310 | ```python 311 | SECRET_KEY = config('SECRET_KEY') 312 | ``` 313 | 314 | We should also move `DEBUG` to the config file. Because the file is a string, 315 | and `DEBUG` expects a bool, we need to cast it: 316 | ```python 317 | DEBUG = config('DEBUG', cast=bool) 318 | ``` 319 | 320 | We do this not for security, but so that it can be changed as needed on a 321 | development machine, without modifying the source code. 322 | 323 | Don’t forget to add it to `.env` as well. 324 | 325 | Test to make sure it still works and debug as needed. 326 | 327 | Before moving on, verify that `.env` is in `.gitignore` and commit. 328 | 329 | ## Creating New Secret Keys 330 | 331 | In case you've already committed a key earlier on accident, you can just 332 | generate a new one in any Python REPL with: 333 | 334 | ```python 335 | import random 336 | ''.join([random.SystemRandom().choice('abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)') for i in range(50)]) # All one line! 337 | ``` 338 | -------------------------------------------------------------------------------- /guides/day2.md: -------------------------------------------------------------------------------- 1 | # Day 2: Admin Interface and SQL 2 | 3 | ## Admin Interface 4 | 5 | Now let’s take a look at the admin functions. 6 | 7 | Start the environment and server: 8 | ``` 9 | pipenv shell 10 | ./manage.py runserver 11 | ``` 12 | 13 | Open the page in the web browser and navigate to the admin page: 14 | `localhost:8000/admin`. You will see a login page, but we don’t have an account 15 | to log in with yet. 16 | 17 | To make an admin account, run: 18 | ``` 19 | ./manage.py createsuperuser 20 | ``` 21 | 22 | Add a user `admin` with whatever password you choose. 23 | 24 | Although it can be tempting to use a short and easy password for things like 25 | this, it is good practice to use a robust passphrase. You don’t want to forget 26 | and leave a superuser account with a weak password and have it pass to 27 | production. 28 | 29 | Run the server and log into the admin account you just created. You will be 30 | able to see the automatically generated users and groups from the database, but 31 | our notes are missing. 32 | 33 | We need to tell the admin interface which tables we're interested in seeing. 34 | 35 | In the `notes/admin.py` file: 36 | 37 | ```python 38 | from .models import Note 39 | ``` 40 | 41 | and register the `Note` model with the admin site with: 42 | 43 | ```python 44 | admin.site.register(Note) 45 | ``` 46 | 47 | Return to the site admin page. `Notes` should now be present. Try adding 48 | and/or editing a few. 49 | 50 | If you want to register more models, you can do so with additional `register()` 51 | calls: 52 | 53 | ```python 54 | admin.site.register(Note) 55 | admin.site.register(PersonalNote) # etc. 56 | ``` 57 | 58 | ## Migrations with New Fields 59 | 60 | It would also be nice to track created and modified dates. 61 | 62 | Open `notes/models.py` and add: 63 | 64 | ```python 65 | created_at = models.DateTimeField(auto_now_add=True) 66 | last_modified = models.DateTimeField(auto_now=True) 67 | ``` 68 | 69 | The argument we are using determines when and how this information should be 70 | updated: `auto_now_add` only sets on create, while `auto_now` will set on both 71 | create and update. 72 | 73 | In the terminal, make the migration: 74 | ``` 75 | ./manage.py makemigrations 76 | ``` 77 | 78 | You will get the following: 79 | ``` 80 | You are trying to add the field ‘created_at’ with ‘auto_now_add=True’ to note 81 | without a default; the database needs something to populate existing rows. 82 | 83 | 1) Provide a one-off default now (will be set on all existing rows) 84 | 2) Quit, and let me add a default in models.py 85 | Select an option: 86 | ``` 87 | 88 | Default can sometimes be specified with: 89 | ```python 90 | foo = whateverField(default=value) 91 | ``` 92 | 93 | Or you can allow the field to be blank with: 94 | ```python 95 | foo = whateverField(blank=True) 96 | ``` 97 | 98 | But this _will not work_ in a `DateTimeField` with `auto_now` or `auto_now_add` 99 | set, so use option 1 with suggested default of `timezone.now`. 100 | 101 | Do the migration: `./manage.py migrate` 102 | 103 | You might notice that the new fields aren't showing up in the admin interface. This is because when you use `auto_now`, the field gets set to read-only, and such fields aren't shown in the panel. 104 | 105 | To get the read-only fields to show up in the interface: 106 | 107 | ```python 108 | class NoteAdmin(admin.ModelAdmin): 109 | readonly_fields=('created_at', 'last_modified') 110 | 111 | # Register your models here. 112 | admin.site.register(Note, NoteAdmin) 113 | ``` 114 | 115 | ## Personal (per-user) Notes 116 | 117 | Next we want to add the ability to handle multiple users, and allow them to have 118 | their own personal notes. 119 | 120 | First, we will create a new model that inherits from another: personal notes. 121 | Open up `notes/models.py` 122 | 123 | To access the built-in user functionality: 124 | ```python 125 | from django.contrib.auth.models import User 126 | ``` 127 | 128 | We could copy and paste the previous notes class to do this, but a better way is 129 | to have it inherit from it and just add the additional fields we need. 130 | 131 | ```python 132 | class PersonalNote(Note): # Inherits from Note! 133 | user = models.ForeignKey(User, on_delete=models.CASCADE) 134 | ``` 135 | 136 | What this is doing is importing Django’s built in user class model with 137 | something called a _foreign key_ to create a reference to data on another table. 138 | It works sort of like a pointer in C. 139 | 140 | `on_delete=models.CASCADE` helps with the integrity of the data. In relational 141 | databases, one of the principles is to protect consistency. There shouldn’t be 142 | an item in one table that references the foreign key of something that has been 143 | removed from another. Check the readme in the repo for more info. 144 | 145 | ## Under the Hood with SQL 146 | 147 | We can take a look in the database with `./manage.py dbshell`. If you get an 148 | error, you may need to install sqlite3 using your preferred method. 149 | 150 | If it is working, the command prompt will change to `sqlite`. 151 | 152 | `.tables` will display a list of tables 153 | 154 | `pragma table_info(notes_note);` will show column names and types for the table 155 | `notes_note`. 156 | 157 | `.headers on` and `.mode column` will adjust some settings to clean up the 158 | presentation if we open a table. 159 | 160 | `SELECT * FROM notes_note;` is a sql command that will select all of the columns 161 | in the notes_note table and display the data present. By convention, sql 162 | commands are often uppercase, but it is actually case insensitive. 163 | 164 | All the notes we have created will be displayed. 165 | 166 | Be _very_ careful with sql commands. The command `DROP` will permanently delete 167 | a table and all of the data inside it without warning. _This language is 168 | powerful and has no mercy_. 169 | 170 | Type `.exit` or `CTRL-D` to get out of dbshell. 171 | 172 | Back in the virtual environment, because we modified the model to add personal 173 | notes, we need to do another migration. 174 | 175 | Complete the migration process as before. 176 | 177 | We also want personal notes to show up on the admin page. Open `admin.py` then 178 | import and register the new class. Remember, you can use tuples for both of 179 | these. Don’t forget to use the extra parentheses inside the register function. 180 | 181 | Take a look at it in `admin`. It should be the same as before, but now we have 182 | a `user` field that is automatically populated. 183 | 184 | We can use the admin interface to add more users in the user table, if we want. 185 | 186 | For now, create a personal note for the admin account. 187 | 188 | Go back to the sql shell, and take a look at the `notes_personalnote` table. 189 | 190 | You’ll need to use the same three commands as above to display the table. Note 191 | that the info here is very different. Instead of having everything, it just has 192 | `user_id` and a foreign key `note_ptr_id`, pointing to a record in the full 193 | notes table. 194 | 195 | Take a look at the `notes_note` table. The rest of the data will be here, 196 | listed under the uuid stored in `note_ptr_id`, a reference by the foreign key. 197 | This is why relational databases are relational. 198 | 199 | The `user_id` is also a foreign key that points to Django's built-in `auth_user` 200 | table. Run a `SELECT` query to look in that table, as well. 201 | 202 | ## Django ORM compared to SQL 203 | 204 | Drop out of the SQLite shell and open a Python shell with `./manage.py shell`. 205 | 206 | Import personal notes: `from notes.models import PersonalNote` 207 | 208 | Pull the list into a variable: `pn = PersonalNote.objects.all()` 209 | 210 | Take a look at the name of the 0th record: `pn[0].user`. Try other fields as 211 | well. 212 | 213 | Django lets us access information that is in multiple tables relatively easily. 214 | The sql details are hidden from us (in a good way!). It does all of the under 215 | the hood operations for us. 216 | -------------------------------------------------------------------------------- /guides/day3.md: -------------------------------------------------------------------------------- 1 | # Day 3: Setting up a RESTful API 2 | 3 | ## Installing the REST Framework 4 | 5 | We’ll be using the django REST framework: http://www.django-rest-framework.org/ 6 | 7 | Open the shell if you aren’t in it and install the framework: 8 | 9 | ``` 10 | pipenv install djangorestframework 11 | ``` 12 | 13 | Next, we need to tell the project about this. Open 14 | `[name_of_project]/settings.py` 15 | 16 | Under `INSTALLED_APPS` add `'rest_framework'` to the list. 17 | 18 | We also need to add some boilerplate to set up permissions: 19 | 20 | ```python 21 | REST_FRAMEWORK = { 22 | 'DEFAULT_PERMISSION_CLASSES': [ 23 | 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly', 24 | ] 25 | } 26 | ``` 27 | 28 | This will allow read/write permissions for logged in users and read only for 29 | anonymous users. 30 | 31 | Launch the server and make sure everything is working, debug, and commit. 32 | 33 | ## Expose the PersonalNotes Model 34 | 35 | Next, we need to add more boilerplate. In the `notes` folder, create a new file 36 | called `api.py`. This will use something called serializers and viewsets to 37 | describe which parts of the model we want to expose to the API. 38 | 39 | First, in `api.py`, import the serializers: 40 | 41 | ```python 42 | from rest_framework import serializers 43 | ``` 44 | 45 | We’ll also need to import the `PersonalNote` class so that we can use it here. 46 | 47 | Convention is to name the serializer classes after what they are serializing. 48 | It will inherit from the specific serializer we are using for this project: 49 | 50 | ```python 51 | class PersonalNoteSerializer(serializers.HyperlinkedModelSerializer): 52 | ``` 53 | 54 | Inside this class, we will make an _inner class_ (nested class) called a `Meta` 55 | to tell it what parts of the model we want to access: 56 | 57 | ```python 58 | # Inner class nested inside PersonalNoteSerializer 59 | class Meta: 60 | model = PersonalNote 61 | fields = ('title', 'content') 62 | ``` 63 | 64 | To visualize this, we will use something called a viewset. Add `viewsets` to 65 | what is being imported from `rest_framework`: 66 | 67 | ```python 68 | from rest_framework import serializers, viewsets 69 | ``` 70 | 71 | Create a new class for this, using the same naming convention as the serializer 72 | and inheriting from `viewsets.ModelViewSet` 73 | 74 | ```python 75 | class PersonalNoteViewSet(viewsets.ModelViewSet): 76 | ``` 77 | 78 | Link this back to the serializer class we made previously: 79 | 80 | ```python 81 | serializer_class = PersonalNoteSerializer 82 | ``` 83 | 84 | Next, add which records to search for. We could use filters here, but for now, 85 | grab all of them: 86 | 87 | ```python 88 | queryset = PersonalNote.objects.all() 89 | ``` 90 | 91 | There isn’t anything we can check just yet to ensure that this is working, but 92 | let’s make sure we haven’t broken anything. 93 | 94 | At this point, we should have: 95 | 96 | ```python 97 | from rest_framework import serializers, viewsets 98 | from .models import PersonalNote 99 | 100 | class PersonalNoteSerializer(serializers.HyperlinkedModelSerializer): 101 | 102 | class Meta: 103 | model = PersonalNote 104 | fields = ('title', 'content') 105 | 106 | class PersonalNoteViewSet(viewsets.ModelViewSet): 107 | serializer_class = PersonalNoteSerializer 108 | queryset = PersonalNote.objects.all() 109 | ``` 110 | 111 | Run the server, debug, commit. 112 | 113 | ## Add Routes 114 | 115 | Lastly, we need to add a route to be able to access this functionality. In the 116 | project folder, open `urls.py`. 117 | 118 | We’ll need to import two things here: router functionality for Django, and the 119 | `PersonalNoteViewSet` we just created. 120 | 121 | ```python 122 | from rest_framework import routers 123 | from notes.api import PersonalNoteViewSet 124 | ``` 125 | 126 | Next, make a default router from the routers package, then register that router: 127 | 128 | ```python 129 | router = routers.DefaultRouter() 130 | router.register(r'notes', PersonalNoteViewSet) 131 | ``` 132 | 133 | This is similar to setting up a route in express, but we’re saying for this 134 | route, this (`PersonalNoteViewSet`) is the data we want to associate with it. 135 | (The `r` means that this is a regular expression, and to interpret the string as 136 | literally as possible--somewhat overkill in this case.) 137 | 138 | Next, we need to add the URL to the `urlpatterns` list. In order to do that, 139 | we'll be using a function called `include()` that we get from `django.urls`: 140 | 141 | ```python 142 | from django.urls import path, include 143 | ``` 144 | 145 | And in `urlpatterns`: 146 | 147 | ```python 148 | path('api/', include(router.urls)), 149 | ``` 150 | 151 | This will set the path to `/api/notes`. We can use `router.register` to add 152 | as many paths as we want this way, without needing to add them to `urlpatterns` 153 | 154 | ## Test the API 155 | 156 | Run the server, navigate to `/api/` and review the information there. Click the 157 | link to `notes` and review that as well. 158 | 159 | Use the admin feature at the bottom to attempt to post a new note. This will 160 | fail. 161 | 162 | The reason is that our `PersonalNote` model requires a username as well. We 163 | need to add that in. 164 | 165 | ## Add the Required `user` Field, Use the Debugger 166 | 167 | We can do that in our serializer by overriding a method from 168 | `serializers.HyperlinkedModelSerializer` called `create`. This method needs to 169 | return a new `PersonalNote` object constructed from the passed-in data, which is 170 | in the `validated_data` parameter, like so by default: 171 | 172 | In `api.py`, `PersonalNoteSerializer`: 173 | 174 | ```python 175 | # !!! Broken code still missing the user field 176 | 177 | def create(self, validated_data): 178 | note = PersonalNote.objects.create(**validated_data) 179 | return note 180 | ``` 181 | 182 | But we need to add the `user` field into the mix. If the user is logged in to 183 | Django through this browser, that information is automatically included in the 184 | request... but where? Let's use the debugger to explore and find out. 185 | 186 | In `api.py`, `PersonalNoteSerializer`: 187 | 188 | ```python 189 | def create(self, validated_data): 190 | import pdb; pdb.set_trace() # Start the debugger here 191 | pass 192 | ``` 193 | 194 | Run this and use the debugger to the data present at this breakpoint. If you 195 | dig into `self`, you will find eventually find a context with a request. As an 196 | educated guess, using what we’ve previously learned about requests, it is fair 197 | to hypothesize that a user is associated with the request. Try it out: 198 | 199 | ```python 200 | self.context['request'].user 201 | ``` 202 | 203 | Exit the debugger and add a new variable in `create` to store the user retrieved 204 | from the location in `self` that we just discovered. Feed it in to `PersonalNote.objects.create` as an additional keyword argument: 205 | 206 | ```python 207 | def create(self, validated_data): 208 | user = self.context['request'].user 209 | note = PersonalNote.objects.create(user=user, **validated_data) 210 | return note 211 | ``` 212 | 213 | This will add the needed data to the create method and allow the form to work. 214 | Return to the `/api/notes/` page and test. 215 | 216 | You will receive a `201 Created` that may appear at first as if the data is 217 | being overwritten. Return to the main list page to confirm that everything is 218 | being saved. 219 | 220 | Debug as needed, then commit. 221 | 222 | ## Filter Results by User 223 | 224 | Finally, we have one major problem remaining. Right now, any user can request 225 | and see all of the notes that are in the database. We need to filter them so 226 | that only the appropriate ones are returned. Return to `api.py`, 227 | `PersonalNoteViewSet`. 228 | 229 | Change `queryset` to initialize with `Note.objects.none()`. This will create 230 | the variable with an empty dictionary of the correct type. To make a decision 231 | based on whether or not the user is anonymous, and return only the notes that 232 | belong to the logged in user, we can override a method called 233 | `get_queryset(self)`. 234 | 235 | Load the user into a variable. This class has access to the `request` directly, 236 | so it can be found with `self.request.user`. 237 | 238 | ```python 239 | def get_queryset(self): 240 | user = self.request.user 241 | ``` 242 | 243 | If `user.is_anonymous`, we can return an empty dictionary of notes. Otherwise, 244 | we can use a filter to return only the correct ones with 245 | `Note.objects.filter(user=user)`. 246 | 247 | ```python 248 | def get_queryset(self): 249 | user = self.request.user 250 | 251 | if user.is_anonymous: 252 | return PersonalNote.objects.none() 253 | else: 254 | return PersonalNote.objects.filter(user=user) 255 | ``` 256 | 257 | Test, debug, and commit. 258 | 259 | ## Set up CORS 260 | 261 | In order to get your site to run well with a front-end, you might need to set up CORS: 262 | 263 | https://github.com/ottoyiu/django-cors-headers 264 | 265 | After installation (_follow the instructions at the link, above!_), setting: 266 | 267 | ```python 268 | CORS_ORIGIN_ALLOW_ALL = True 269 | ``` 270 | 271 | should be enough. 272 | 273 | -------------------------------------------------------------------------------- /guides/day4.md: -------------------------------------------------------------------------------- 1 | -------------------- 2 | # Day 4: Token Auth for REST 3 | 4 | The Django Rest Framework also provides token authorization. We will use this 5 | to allow other users to login and access the data specific to them. Information 6 | can be found here: 7 | 8 | http://www.django-rest-framework.org/api-guide/authentication/#authentication 9 | 10 | 11 | ## Set up Token Authentication 12 | 13 | Open `settings.py`. 14 | 15 | To `INSTALLED_APPS`, add `rest_framework.authtoken`. 16 | 17 | If you need them elsewhere, immediately before the boilerplate for `REST_FRAMEWORK`, import `SessionAuthentication`, `BasicAuthentication`, and `TokenAuthentication` from `rest_framework.authentication` 18 | 19 | In `REST_FRAMEWORK`, add: 20 | 21 | ```python 22 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 23 | 'rest_framework.authentication.BasicAuthentication', 24 | 'rest_framework.authentication.SessionAuthentication', 25 | 'rest_framework.authentication.TokenAuthentication', 26 | ), 27 | ``` 28 | 29 | ## Set up the Route 30 | 31 | Next, we need to set up the route to authenticate users. In `urls.py`: 32 | 33 | Import `re_path` from `django.urls` and `views` from `rest_framework.authtoken` 34 | 35 | ```python 36 | from django.urls import path, include, re_path 37 | from rest_framework.authtoken import views 38 | ``` 39 | 40 | Then add the endpoint in `urls.py` by adding the `api-token-auth/` route to 41 | `urlpatterns`: 42 | 43 | ```python 44 | re_path(r'^api-token-auth/', views.obtain_auth_token) 45 | ``` 46 | 47 | The `^` is means "match the beginning of the string" in a regular expression. 48 | The `re_path` function is just like `path`, except it interprets the endpoint as 49 | a regex instead of a fixed string. 50 | 51 | Do a migration to set up the database. 52 | 53 | ## Test the Endpoint 54 | 55 | We can test this on the bash command line with the [curl](https://curl.haxx.se/) 56 | utility that you might already have installed. (Postman also works.) 57 | 58 | Mac/Linux: 59 | ``` 60 | # The following makes a POST request with the given JSON payload: 61 | 62 | curl -X POST -H "Content-Type: application/json" -d '{"username":"admin", "password":"PASSWORD"}' http://127.0.0.1:8000/api-token-auth/ 63 | ``` 64 | 65 | Windows command prompt (or other platform if the above doesn't work): 66 | 67 | ``` 68 | # Windows needs some more double quotes and escaping of the payload 69 | 70 | curl -X POST -H "Content-Type: application/json" -d "{\"username\":\"admin\", \"password\":\"PASSWORD\"}" http://127.0.0.1:8000/api-token-auth/ 71 | ``` 72 | 73 | PowerShell has its own thing independent of `curl`: 74 | 75 | ``` 76 | Invoke-WebRequest http://localhost:8000/api-token-auth/ -Method Post -ContentType "application/json" -Body '{"username":"USER", "password":"PASS"}' -UseBasicParsing 77 | ``` 78 | 79 | If you get back a very large amount of html and other text, you have an error. 80 | Scroll back up and google the error displayed just under your console command 81 | for help troubleshooting. Many of the errors you can get here are easy to do, 82 | common, and relatively easy to google for information on how to fix. 83 | 84 | You should get back one line with a token, for example: 85 | 86 | ```json 87 | {"token":"da51ccf5274050cd7332d184246d7d0775dc79e2"} 88 | ``` 89 | 90 | Your token will be different. Try it out with your token: 91 | 92 | ``` 93 | curl -v -H 'Authorization: Token da51ccf5274050cd7332d184246d7d0775dc79e2' http://127.0.0.1:8000/api/notes/ 94 | ``` 95 | 96 | Or, in PowerShell: 97 | 98 | ``` 99 | Invoke-WebRequest http://localhost:8000/api/notes/ -Headers @{"Authorization"="Token da51ccf5274050cd7332d184246d7d0775dc79e2"} 100 | ``` 101 | 102 | Note that the trailing `/` on the URL matters. You will get a 301 redirect if 103 | you don’t add it here. 104 | 105 | When using Axios to send the request, set the header here: 106 | 107 | ```javascript 108 | axios.post('http://127.0.0.1:8000/api/notes/', data, { 109 | headers: { 110 | 'Authorization': 'Token da51ccf5274050cd7332d184246d7d0775dc79e2', 111 | } 112 | } 113 | ``` 114 | -------------------------------------------------------------------------------- /guides/trouble.md: -------------------------------------------------------------------------------- 1 | # Common Errors, Gotchas, and Troubleshooting 2 | 3 | Django is a powerful tool, but it can be a bit finicky. This doubles when trying to deploy to Heroku. Below are some common mistakes, errors, and resolutions. 4 | 5 | ## Common Mistakes 6 | * Don't forget to include the `.` when creating your project! If you miss this, everything will work until you try to deploy, then you will have a large number of problems with commands not working as documented. 7 | * The deployment instructions require you to consult outside documentation is several spots to properly configure some of the third party packages we will be using. 8 | * You will be using Sqlite3 locally and Postgres when deployed to Heroku. This very rarely causes minor issues, such as Postgres having a smaller max INT than Sqlite3. However, it is much easier than trying to get Postgres working locally. 9 | 10 | ## `pipenv` error `'module' object is not callable` 11 | 12 | If you're running `pipenv` and you get an error that ends like this: 13 | 14 | ``` 15 | "/usr/local/Cellar/pipenv/2018.7.1/libexec/lib/python3.7/site-packages/pipenv/vendor/requirementslib/models/requirements.py", line 704, in from_line 16 | line, extras = _strip_extras(line) 17 | TypeError: 'module' object is not callable 18 | ``` 19 | 20 | it might be time to upgrade `pipenv`. Make sure that `pipenv --version` is 21 | outputting at least `2018.10.13`. 22 | 23 | ## `pipenv` error `Found existing installation: [package name]` 24 | 25 | Add the following option to ignore existing installs: `--ignore-installed` 26 | 27 | ``` 28 | pip install pipenv --upgrade --ignore-installed 29 | ``` 30 | 31 | ## `./manage.py` error `from exc` 32 | 33 | If you get this: 34 | 35 | ``` 36 | ) from exc 37 | ^ 38 | SyntaxError: invalid syntax 39 | ``` 40 | 41 | it usually means Python 2.x is running instead of Python 3. This, in turn, 42 | usually means you've forgotten to `pipenv shell` into your virtual environment. 43 | 44 | Run `python --version` and make sure it says `3.`-something. 45 | 46 | ## `./manage.py dbshell` not launching 47 | 48 | _Chief. Windows_ 49 | 50 | Make sure you have your SQLite3 package installed and in your path. 51 | 52 | A recommended way to install SQLite3 on Windows is [with 53 | chocolatey](https://chocolatey.org/packages?q=sqlite). 54 | 55 | ## `curl` not working from PowerShell 56 | 57 | By default, PowerShell uses `curl` as an alias for its own `Invoke-WebRequest`. If you've installed `curl` and want to use it instead, you have to unalias it with 58 | 59 | ```powershell 60 | Remove-Item alias:curl 61 | ``` 62 | 63 | [Here are some instructions for removing the alias 64 | permanently](https://superuser.com/questions/883914/how-do-i-permanently-remove-a-default-powershell-alias) 65 | 66 | ## VS Code not recognizing Django imports properly 67 | 68 | Enable Django linting by adding the following to your VS Code workplace 69 | settings: 70 | 71 | ``` 72 | "python.linting.pylingArgs":["--load-plugins","pylint_django"] 73 | ``` 74 | 75 | ## Using PowerShell in VS Code terminal 76 | 77 | If you prefer it, [here are some instructions on setting it 78 | up](https://code.visualstudio.com/docs/editor/integrated-terminal). 79 | --------------------------------------------------------------------------------