diff --git a/.gitignore b/.gitignore index 894a44cc0..7bff73183 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,4 @@ venv.bak/ # mypy .mypy_cache/ + diff --git a/Pipfile b/Pipfile new file mode 100644 index 000000000..190afb452 --- /dev/null +++ b/Pipfile @@ -0,0 +1,18 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +django = "*" +python-decouple = "*" +djangorestframework = "*" +gunicorn = "*" +"psycopg2-binary" = "*" +dj-database-url = "*" +whitenoise = "*" + +[dev-packages] + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 000000000..9389d5bec --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,111 @@ +{ + "_meta": { + "hash": { + "sha256": "8afcbb900798e3365aaa12966ff1c4a545134cbc204818e57f81d4504337595e" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "dj-database-url": { + "hashes": [ + "sha256:4aeaeb1f573c74835b0686a2b46b85990571159ffc21aa57ecd4d1e1cb334163", + "sha256:851785365761ebe4994a921b433062309eb882fedd318e1b0fcecc607ed02da9" + ], + "index": "pypi", + "version": "==0.5.0" + }, + "django": { + "hashes": [ + "sha256:068d51054083d06ceb32ce02b7203f1854256047a0d58682677dd4f81bceabd7", + "sha256:55409a056b27e6d1246f19ede41c6c610e4cab549c005b62cbeefabc6433356b" + ], + "index": "pypi", + "version": "==2.1.4" + }, + "djangorestframework": { + "hashes": [ + "sha256:607865b0bb1598b153793892101d881466bd5a991de12bd6229abb18b1c86136", + "sha256:63f76cbe1e7d12b94c357d7e54401103b2e52aef0f7c1650d6c820ad708776e5" + ], + "index": "pypi", + "version": "==3.9.0" + }, + "gunicorn": { + "hashes": [ + "sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471", + "sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3" + ], + "index": "pypi", + "version": "==19.9.0" + }, + "psycopg2-binary": { + "hashes": [ + "sha256:036bcb198a7cc4ce0fe43344f8c2c9a8155aefa411633f426c8c6ed58a6c0426", + "sha256:1d770fcc02cdf628aebac7404d56b28a7e9ebec8cfc0e63260bd54d6edfa16d4", + "sha256:1fdc6f369dcf229de6c873522d54336af598b9470ccd5300e2f58ee506f5ca13", + "sha256:21f9ddc0ff6e07f7d7b6b484eb9da2c03bc9931dd13e36796b111d631f7135a3", + "sha256:247873cda726f7956f745a3e03158b00de79c4abea8776dc2f611d5ba368d72d", + "sha256:3aa31c42f29f1da6f4fd41433ad15052d5ff045f2214002e027a321f79d64e2c", + "sha256:475f694f87dbc619010b26de7d0fc575a4accf503f2200885cc21f526bffe2ad", + "sha256:4b5e332a24bf6e2fda1f51ca2a57ae1083352293a08eeea1fa1112dc7dd542d1", + "sha256:570d521660574aca40be7b4d532dfb6f156aad7b16b5ed62d1534f64f1ef72d8", + "sha256:59072de7def0690dd13112d2bdb453e20570a97297070f876fbbb7cbc1c26b05", + "sha256:5f0b658989e918ef187f8a08db0420528126f2c7da182a7b9f8bf7f85144d4e4", + "sha256:649199c84a966917d86cdc2046e03d536763576c0b2a756059ae0b3a9656bc20", + "sha256:6645fc9b4705ae8fbf1ef7674f416f89ae1559deec810f6dd15197dfa52893da", + "sha256:6872dd54d4e398d781efe8fe2e2d7eafe4450d61b5c4898aced7610109a6df75", + "sha256:6ce34fbc251fc0d691c8d131250ba6f42fd2b28ef28558d528ba8c558cb28804", + "sha256:73920d167a0a4d1006f5f3b9a3efce6f0e5e883a99599d38206d43f27697df00", + "sha256:8a671732b87ae423e34b51139628123bc0306c2cb85c226e71b28d3d57d7e42a", + "sha256:8d517e8fda2efebca27c2018e14c90ed7dc3f04d7098b3da2912e62a1a5585fe", + "sha256:9475a008eb7279e20d400c76471843c321b46acacc7ee3de0b47233a1e3fa2cf", + "sha256:96947b8cd7b3148fb0e6549fcb31258a736595d6f2a599f8cd450e9a80a14781", + "sha256:abf229f24daa93f67ac53e2e17c8798a71a01711eb9fcdd029abba8637164338", + "sha256:b1ab012f276df584beb74f81acb63905762c25803ece647016613c3d6ad4e432", + "sha256:b22b33f6f0071fe57cb4e9158f353c88d41e739a3ec0d76f7b704539e7076427", + "sha256:b3b2d53274858e50ad2ffdd6d97ce1d014e1e530f82ec8b307edd5d4c921badf", + "sha256:bab26a729befc7b9fab9ded1bba9c51b785188b79f8a2796ba03e7e734269e2e", + "sha256:daa1a593629aa49f506eddc9d23dc7f89b35693b90e1fbcd4480182d1203ea90", + "sha256:dd111280ce40e89fd17b19c1269fd1b74a30fce9d44a550840e86edb33924eb8", + "sha256:e0b86084f1e2e78c451994410de756deba206884d6bed68d5a3d7f39ff5fea1d", + "sha256:eb86520753560a7e89639500e2a254bb6f683342af598088cb72c73edcad21e6", + "sha256:ff18c5c40a38d41811c23e2480615425c97ea81fd7e9118b8b899c512d97c737" + ], + "index": "pypi", + "version": "==2.7.6.1" + }, + "python-decouple": { + "hashes": [ + "sha256:1317df14b43efee4337a4aa02914bf004f010cd56d6c4bd894e6474ec8c4fe2d" + ], + "index": "pypi", + "version": "==3.1" + }, + "pytz": { + "hashes": [ + "sha256:31cb35c89bd7d333cd32c5f278fca91b523b0834369e757f4c5641ea252236ca", + "sha256:8e0f8568c118d3077b46be7d654cc8167fa916092e28320cde048e54bfc9f1e6" + ], + "version": "==2018.7" + }, + "whitenoise": { + "hashes": [ + "sha256:118ab3e5f815d380171b100b05b76de2a07612f422368a201a9ffdeefb2251c1", + "sha256:42133ddd5229eeb6a0c9899496bdbe56c292394bf8666da77deeb27454c0456a" + ], + "index": "pypi", + "version": "==4.1.2" + } + }, + "develop": {} +} diff --git a/Procfile b/Procfile new file mode 100644 index 000000000..be138390c --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: gunicorn djorg.wsgi --log-file - \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 9203be49d..000000000 --- a/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# Intro to Django - -Fork this repo to use for your projects this week. - -## Reading - -* [Common Errors and Troubleshooting](guides/trouble.md) - -* [Day 1: Intro](guides/day1.md) -* [Day 2: Admin Interface and SQL](guides/day2.md) -* [Day 3: Setting up a RESTful API](guides/day3.md) -* [Day 4: Token Auth for REST](guides/day4.md) - -### External links - -* [Virtual Environments Primer](https://realpython.com/python-virtual-environments-a-primer/) - -## Deliverables - -* Implement an app similar to the `notes` app, but with data of your choosing. -* Submit a file `models.txt` that describes (to other developers) what data you are storing in the database. -* The app needs to support authentication and expose a RESTful API to the data. - -## Additional Deliverables - -(In arbitrary order.) - -* Add filters and search capabilities to your REST API. -* Add ability to upload attachments to your records; e.g. images, etc. -* Add ability to change an existing record, not just GET and POST. -* Add a Django front end to the data. -* Brainstorm a list of 10 additional features users would find useful. -* Implement the brainstormed list. diff --git a/db.sqlite32boi b/db.sqlite32boi new file mode 100644 index 000000000..acc1952de Binary files /dev/null and b/db.sqlite32boi differ diff --git a/djorg/__init__.py b/djorg/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/djorg/settings.py b/djorg/settings.py new file mode 100644 index 000000000..14c9041e7 --- /dev/null +++ b/djorg/settings.py @@ -0,0 +1,144 @@ +""" +Django settings for djorg project. + +Generated by 'django-admin startproject' using Django 2.1.4. + +For more information on this file, see +https://docs.djangoproject.com/en/2.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/2.1/ref/settings/ +""" + +import os + +from decouple import config +import dj_database_url + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = config('SECRET_KEY') + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = config('DEBUG', cast=bool) + +ALLOWED_HOSTS = ['*'] + + +# Application definition + +INSTALLED_APPS = [ + 'notes', + 'rest_framework', + 'rest_framework.authtoken', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +MIDDLEWARE = [ + 'whitenoise.middleware.WhiteNoiseMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'djorg.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'djorg.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/2.1/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + +DATABASES['default'] = dj_database_url.config(conn_max_age=600) + +# Password validation +# https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/2.1/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/2.1/howto/static-files/ + +STATIC_URL = '/static/' + +STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' + +STATIC_ROOT = os.path.join(BASE_DIR, 'static') + +REST_FRAMEWORK = { + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly', + ], + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework.authentication.BasicAuthentication', + 'rest_framework.authentication.SessionAuthentication', + 'rest_framework.authentication.TokenAuthentication', + ), +} + diff --git a/djorg/urls.py b/djorg/urls.py new file mode 100644 index 000000000..ba2b92339 --- /dev/null +++ b/djorg/urls.py @@ -0,0 +1,30 @@ +"""djorg URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/2.1/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include + +from rest_framework import routers +from notes.api import PersonalNoteViewSet + +from rest_framework.authtoken import views +router = routers.DefaultRouter() +router.register('notes', PersonalNoteViewSet) + +urlpatterns = [ + path('admin/', admin.site.urls), + path('api/', include(router.urls)), + path('api-token-auth/', views.obtain_auth_token), +] diff --git a/djorg/wsgi.py b/djorg/wsgi.py new file mode 100644 index 000000000..7bb827bb0 --- /dev/null +++ b/djorg/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for djorg project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djorg.settings') + +application = get_wsgi_application() diff --git a/guides/day1.md b/guides/day1.md deleted file mode 100644 index cd54d6429..000000000 --- a/guides/day1.md +++ /dev/null @@ -1,337 +0,0 @@ -# Day 1: Intro - -## Summary - -* Get pipenv installed -* Clone your repo - * (If you cloned the Hello-Django repo, delete the file `requirements.txt`!) -* Go to your repo root directory -* `pipenv --three` -* `pipenv install` -* `pipenv shell` -* `pipenv install django` -* `django-admin startproject djorg .` -* `django-admin startapp notes` -* `./manage.py runserver` -* `./manage.py showmigrations` -* `./manage.py migrate` -* `./manage.py runserver` -* Add model to `notes/models.py` -* Add `'notes'` to `INSTALLED_APPS` in `djorg/settings.py` -* `./manage.py showmigrations` -* `./manage.py makemigrations` -* `./manage.py showmigrations` -* `./manage.py migrate` -* `./manage.py shell` - * `from notes.models import Note` - * `n = Note(title=”example”, content=”This is a test.”)` - * `n.save()` - * `exit()` -* `./manage.py shell` - * `from notes.models import Note` - * `x = Note.objects.all()` - * `x[0]` - * `x[0].content` - * `exit()` -* `pipenv install python-decouple` -* Add config information to `settings.py` and `.env` - - -## Setting up a Virtual Environment - -Check Python version and install or upgrade if less than 3.5.x. - -Check Pip version and install or upgrade if less than 10.x. - -* Mac/Linux - Option A: `sudo -H pip3 install --upgrade pip` -* Mac/Linux - Option B: `brew upgrade python` -* PC - `python -m pip install --upgrade pip` - - -Check Pipenv version and install or upgrade if less than 2018.x -
*(2018.10.13 as of November 6, 2018)* - - -Normally you'd make a repo with a README and a Python gitignore, but since we're -going to be using pull requests to turn things in, just fork this repo instead. - - -Clone repo on to local machine. - -In the terminal, navigate to root folder of repo. - -Create pipenv virtual environment: - -``` -pipenv --three -``` - -* The `--three` option tells it to use Python3 -* This is similar to using `npm`/`yarn` - - -Verify that the `Pipfile` was created in the root of the repo. - - -Activate pipenv with `pipenv shell` - -* You should see the command line change to the name of your repo/folder - followed by a dash and a random string. -* We are using pipenv because it is newer and more robust. Uses a lockfile - similar to npm/yarn. Easier to get into and out of shell. -* To get back in, use `pipenv shell` from the root directory of the project. - -## To Start a Django Project and App - -Once you are in the virtual environment, install django: - -``` -pipenv install django -``` - -* We are using a virtual environment instead of installing globally because - installing globally would be like using npm/yarn install globally and - installing all the packages on everything. - -Add `Pipfile` and `Pipfile.lock` to the repo with `git add Pipfile*` and commit -with `git commit -m "added pipfiles"`. - -Start a project with `django-admin startproject [name_of_project] .` - -* Replace [nameofproject] with the name of your project -* The . tells it to create the project in the current directory. Otherwise, it - would create a project in a subdirectory called [name_of_project]. We don’t - need that because we want the repo folder to be the root - -Verify that the [name_of_project] folder was created and has boilerplate files -such as `__init__.py`. - -The project is what it was named above. A project is made up of a collection of -apps. It can have one or many. - - -Create an app with `django-admin startapp [name_of_app]` - -* For the first project, we are naming the app notes -* Name it differently as appropriate if you are following this to set up, but - working on something else. - -## Start the Server - -Verify that the [name_of_app] subdirectory has been created - -Test by navigating to the project folder root/[name_of_project] and running -`./manage.py runserver` - -* This should launch the animated rocket default page -* Take note of the warning about unapplied migrations. We will fix that in a moment - -Django makes it easier to make changes to databases. This is called migration(s). - - -## Migrations - -Run `./manage.py showmigrations`. This will show a list of outstanding -changes that need to occur. - -To take a closer look at what is being done, you can look at the SQL queries -that Django is building. _This step is entirely optional, and is only for the -curious--which should be you!_ - -``` -./manage.py sqlmigrate [package_name] [migration_id] -``` - -for example - -``` -./manage.py sqlmigrate admin 0001_initial -``` - -* This will display a large number of sql commands that may not make sense if - you are not yet familiar with SQL. -* This doesn’t actually do anything. It just displays info. -* These are all the data structures that your python code has created. Django - turns this into sql tables, etc. for you. (If you’ve ever done this manually, - you know how awesome that is :) ) - - -To actually run the migrations, use: - -``` -./manage.py migrate -``` - -Check them by showing migrations again: `./manage.py showmigrations` - -* The list of migrations should show an `x` for each item. - -Run the server again and confirm that the migration warning is not present. -There won’t be a change to the actual page that renders. - -## Adding Data Models - -In the `notes` folder, open `models.py`. - -Create a class called `notes` that inherits from `models.Model`: - -```python -class Note(models.Model): -``` - -This gives our new class access to all of the built-in functionality in `models.Model`. - -Think about the data that we need for standard web notes functionality. We might -want a title, body, some timestamps, etc. We can use the docs to find the types -of things we can add: - -https://docs.djangoproject.com/en/2.0/ref/models/ - -Add the following variables to the class: - -```python -class Note(models.Model): - title = models.CharField(max_length=200) - content = models.TextField(blank=True) -``` - -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. - -We also need something to serve as a unique identifier for each record. We’ll -use something called a UUID for this: -[https://en.wikipedia.org/wiki/Universally_unique_identifier](https://en.wikipedia.org/wiki/Universally_unique_identifier). - -Add a UUID to serve as a key for each record. - -* First, import the library: - ```python - from uuid import uuid4 - ``` -* Second, add the field: - ```python - id = models.UUIDField(primary_key=True, default=uuid4, editable=False) - ``` - -* Primary key is how the database tracks records. -* Default calls a function to randomly generate a unique identifier. -* We make editable false because we never want to change the key. -* Put it at the top of the list of fields because it’s sort of like the index - for the record. - -Next, we need to tell the project that the app exists. Open `settings.py` from -the project folder. - -Find the section for `INSTALLED_APPS` and add `'notes'`, or other apps as -appropriate. - -In the console, check for migrations again with `./manage.py -showmigrations`. The notes app should show up in the list now, but it has no -migrations. - -To generate the migrations, run: - -``` -./manage.py makemigrations -``` - -If you get an error that there are no changes to make, double-check that you -have saved `models.py`. - -Show migrations again to make sure they appear, then do the migration: -``` -./manage.py migrate -``` - -## Adding Data with the Python Shell - -`manage.py` has its own shell. Run `./manage.py shell` to bring up a Python repl. - -* The input line should change to `>>>` - -Import the notes class into the repl: -```python -from notes.models import Note -``` - -Create a new note with: -```python -n = Note(title="example", content="This is a test.") -``` - -Check by the name of your variable to make sure worked: -```python -n -``` - -We can use a built in function from models (which `Note` inherited from!) to -save this to the database: -```python -n.save() -``` - -Exit the terminal, then restart it - `exit()` then `./manage.py shell` - -We have to import the `Note` class again, using the same command as before. - -There is another built in method that will retrieve all existing objects of a -class: `Note.objects.all()` - -Use this to save the data back into a variable named `b` and explore. - -## Moving the Secret Key to `.env` - -Take a look at that secret key in `settings.py`. - -We want to move this out of the settings file so that it doesn’t get checked -into source control. We’ll move it to another file, which everyone on the -project will need a copy of, but it won’t be in the repo itself. - -We’re going to make use of a module called Python Decouple by installing it in -the virtual environment: -``` -pipenv install python-decouple -``` - -Once it’s installed, we can bring it into `settings.py` with: -```python -from decouple import config -``` - -Pull up the docs for Python Decouple and take a look at the usage and rationale. -There is an example for how to use it with Django. Follow that to remove the key -from settings. - -Add the key your `.env` file (creating it if you have to): -``` -SECRET_KEY='...whatever it was in the settings file...' -``` - -Then change the line in `settings.py` to: -```python -SECRET_KEY = config('SECRET_KEY') -``` - -We should also move `DEBUG` to the config file. Because the file is a string, -and `DEBUG` expects a bool, we need to cast it: -```python -DEBUG = config('DEBUG', cast=bool) -``` - -We do this not for security, but so that it can be changed as needed on a -development machine, without modifying the source code. - -Don’t forget to add it to `.env` as well. - -Test to make sure it still works and debug as needed. - -Before moving on, verify that `.env` is in `.gitignore` and commit. - -## Creating New Secret Keys - -In case you've already committed a key earlier on accident, you can just -generate a new one in any Python REPL with: - -```python -import random -''.join([random.SystemRandom().choice('abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)') for i in range(50)]) # All one line! -``` diff --git a/guides/day2.md b/guides/day2.md deleted file mode 100644 index 7d2333937..000000000 --- a/guides/day2.md +++ /dev/null @@ -1,215 +0,0 @@ -# Day 2: Admin Interface and SQL - -## Admin Interface - -Now let’s take a look at the admin functions. - -Start the environment and server: -``` -pipenv shell -./manage.py runserver -``` - -Open the page in the web browser and navigate to the admin page: -`localhost:8000/admin`. You will see a login page, but we don’t have an account -to log in with yet. - -To make an admin account, run: -``` -./manage.py createsuperuser -``` - -Add a user `admin` with whatever password you choose. - -Although it can be tempting to use a short and easy password for things like -this, it is good practice to use a robust passphrase. You don’t want to forget -and leave a superuser account with a weak password and have it pass to -production. - -Run the server and log into the admin account you just created. You will be -able to see the automatically generated users and groups from the database, but -our notes are missing. - -We need to tell the admin interface which tables we're interested in seeing. - -In the `notes/admin.py` file: - -```python -from .models import Note -``` - -and register the `Note` model with the admin site with: - -```python -admin.site.register(Note) -``` - -Return to the site admin page. `Notes` should now be present. Try adding -and/or editing a few. - -If you want to register more models, you can do so with additional `register()` -calls: - -```python -admin.site.register(Note) -admin.site.register(PersonalNote) # etc. -``` - -## Migrations with New Fields - -It would also be nice to track created and modified dates. - -Open `notes/models.py` and add: - -```python -created_at = models.DateTimeField(auto_now_add=True) -last_modified = models.DateTimeField(auto_now=True) -``` - -The argument we are using determines when and how this information should be -updated: `auto_now_add` only sets on create, while `auto_now` will set on both -create and update. - -In the terminal, make the migration: -``` -./manage.py makemigrations -``` - -You will get the following: -``` -You are trying to add the field ‘created_at’ with ‘auto_now_add=True’ to note -without a default; the database needs something to populate existing rows. - -1) Provide a one-off default now (will be set on all existing rows) -2) Quit, and let me add a default in models.py -Select an option: -``` - -Default can sometimes be specified with: -```python -foo = whateverField(default=value) -``` - -Or you can allow the field to be blank with: -```python -foo = whateverField(blank=True) -``` - -But this _will not work_ in a `DateTimeField` with `auto_now` or `auto_now_add` -set, so use option 1 with suggested default of `timezone.now`. - -Do the migration: `./manage.py migrate` - -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. - -To get the read-only fields to show up in the interface: - -```python -class NoteAdmin(admin.ModelAdmin): - readonly_fields=('created_at', 'last_modified') - -# Register your models here. -admin.site.register(Note, NoteAdmin) -``` - -## Personal (per-user) Notes - -Next we want to add the ability to handle multiple users, and allow them to have -their own personal notes. - -First, we will create a new model that inherits from another: personal notes. -Open up `notes/models.py` - -To access the built-in user functionality: -```python -from django.contrib.auth.models import User -``` - -We could copy and paste the previous notes class to do this, but a better way is -to have it inherit from it and just add the additional fields we need. - -```python -class PersonalNote(Note): # Inherits from Note! - user = models.ForeignKey(User, on_delete=models.CASCADE) -``` - -What this is doing is importing Django’s built in user class model with -something called a _foreign key_ to create a reference to data on another table. -It works sort of like a pointer in C. - -`on_delete=models.CASCADE` helps with the integrity of the data. In relational -databases, one of the principles is to protect consistency. There shouldn’t be -an item in one table that references the foreign key of something that has been -removed from another. Check the readme in the repo for more info. - -## Under the Hood with SQL - -We can take a look in the database with `./manage.py dbshell`. If you get an -error, you may need to install sqlite3 using your preferred method. - -If it is working, the command prompt will change to `sqlite`. - -`.tables` will display a list of tables - -`pragma table_info(notes_note);` will show column names and types for the table -`notes_note`. - -`.headers on` and `.mode column` will adjust some settings to clean up the -presentation if we open a table. - -`SELECT * FROM notes_note;` is a sql command that will select all of the columns -in the notes_note table and display the data present. By convention, sql -commands are often uppercase, but it is actually case insensitive. - -All the notes we have created will be displayed. - -Be _very_ careful with sql commands. The command `DROP` will permanently delete -a table and all of the data inside it without warning. _This language is -powerful and has no mercy_. - -Type `.exit` or `CTRL-D` to get out of dbshell. - -Back in the virtual environment, because we modified the model to add personal -notes, we need to do another migration. - -Complete the migration process as before. - -We also want personal notes to show up on the admin page. Open `admin.py` then -import and register the new class. Remember, you can use tuples for both of -these. Don’t forget to use the extra parentheses inside the register function. - -Take a look at it in `admin`. It should be the same as before, but now we have -a `user` field that is automatically populated. - -We can use the admin interface to add more users in the user table, if we want. - -For now, create a personal note for the admin account. - -Go back to the sql shell, and take a look at the `notes_personalnote` table. - -You’ll need to use the same three commands as above to display the table. Note -that the info here is very different. Instead of having everything, it just has -`user_id` and a foreign key `note_ptr_id`, pointing to a record in the full -notes table. - -Take a look at the `notes_note` table. The rest of the data will be here, -listed under the uuid stored in `note_ptr_id`, a reference by the foreign key. -This is why relational databases are relational. - -The `user_id` is also a foreign key that points to Django's built-in `auth_user` -table. Run a `SELECT` query to look in that table, as well. - -## Django ORM compared to SQL - -Drop out of the SQLite shell and open a Python shell with `./manage.py shell`. - -Import personal notes: `from notes.models import PersonalNote` - -Pull the list into a variable: `pn = PersonalNote.objects.all()` - -Take a look at the name of the 0th record: `pn[0].user`. Try other fields as -well. - -Django lets us access information that is in multiple tables relatively easily. -The sql details are hidden from us (in a good way!). It does all of the under -the hood operations for us. diff --git a/guides/day3.md b/guides/day3.md deleted file mode 100644 index 8749cde63..000000000 --- a/guides/day3.md +++ /dev/null @@ -1,272 +0,0 @@ -# Day 3: Setting up a RESTful API - -## Installing the REST Framework - -We’ll be using the django REST framework: http://www.django-rest-framework.org/ - -Open the shell if you aren’t in it and install the framework: - -``` -pipenv install djangorestframework -``` - -Next, we need to tell the project about this. Open -`[name_of_project]/settings.py` - -Under `INSTALLED_APPS` add `'rest_framework'` to the list. - -We also need to add some boilerplate to set up permissions: - -```python -REST_FRAMEWORK = { - 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly', - ] -} -``` - -This will allow read/write permissions for logged in users and read only for -anonymous users. - -Launch the server and make sure everything is working, debug, and commit. - -## Expose the PersonalNotes Model - -Next, we need to add more boilerplate. In the `notes` folder, create a new file -called `api.py`. This will use something called serializers and viewsets to -describe which parts of the model we want to expose to the API. - -First, in `api.py`, import the serializers: - -```python -from rest_framework import serializers -``` - -We’ll also need to import the `PersonalNote` class so that we can use it here. - -Convention is to name the serializer classes after what they are serializing. -It will inherit from the specific serializer we are using for this project: - -```python -class PersonalNoteSerializer(serializers.HyperlinkedModelSerializer): -``` - -Inside this class, we will make an _inner class_ (nested class) called a `Meta` -to tell it what parts of the model we want to access: - -```python - # Inner class nested inside PersonalNoteSerializer - class Meta: - model = PersonalNote - fields = ('title', 'content') -``` - -To visualize this, we will use something called a viewset. Add `viewsets` to -what is being imported from `rest_framework`: - -```python -from rest_framework import serializers, viewsets -``` - -Create a new class for this, using the same naming convention as the serializer -and inheriting from `viewsets.ModelViewSet` - -```python -class PersonalNoteViewSet(viewsets.ModelViewSet): -``` - -Link this back to the serializer class we made previously: - -```python - serializer_class = PersonalNoteSerializer -``` - -Next, add which records to search for. We could use filters here, but for now, -grab all of them: - -```python -queryset = PersonalNote.objects.all() -``` - -There isn’t anything we can check just yet to ensure that this is working, but -let’s make sure we haven’t broken anything. - -At this point, we should have: - -```python -from rest_framework import serializers, viewsets -from .models import PersonalNote - -class PersonalNoteSerializer(serializers.HyperlinkedModelSerializer): - - class Meta: - model = PersonalNote - fields = ('title', 'content') - -class PersonalNoteViewSet(viewsets.ModelViewSet): - serializer_class = PersonalNoteSerializer - queryset = PersonalNote.objects.all() -``` - -Run the server, debug, commit. - -## Add Routes - -Lastly, we need to add a route to be able to access this functionality. In the -project folder, open `urls.py`. - -We’ll need to import two things here: router functionality for Django, and the -`PersonalNoteViewSet` we just created. - -```python -from rest_framework import routers -from notes.api import PersonalNoteViewSet -``` - -Next, make a default router from the routers package, then register that router: - -```python -router = routers.DefaultRouter() -router.register(r'notes', PersonalNoteViewSet) -``` - -This is similar to setting up a route in express, but we’re saying for this -route, this (`PersonalNoteViewSet`) is the data we want to associate with it. -(The `r` means that this is a regular expression, and to interpret the string as -literally as possible--somewhat overkill in this case.) - -Next, we need to add the URL to the `urlpatterns` list. In order to do that, -we'll be using a function called `include()` that we get from `django.urls`: - -```python -from django.urls import path, include -``` - -And in `urlpatterns`: - -```python - path('api/', include(router.urls)), -``` - -This will set the path to `/api/notes`. We can use `router.register` to add -as many paths as we want this way, without needing to add them to `urlpatterns` - -## Test the API - -Run the server, navigate to `/api/` and review the information there. Click the -link to `notes` and review that as well. - -Use the admin feature at the bottom to attempt to post a new note. This will -fail. - -The reason is that our `PersonalNote` model requires a username as well. We -need to add that in. - -## Add the Required `user` Field, Use the Debugger - -We can do that in our serializer by overriding a method from -`serializers.HyperlinkedModelSerializer` called `create`. This method needs to -return a new `PersonalNote` object constructed from the passed-in data, which is -in the `validated_data` parameter, like so by default: - -In `api.py`, `PersonalNoteSerializer`: - -```python - # !!! Broken code still missing the user field - - def create(self, validated_data): - note = PersonalNote.objects.create(**validated_data) - return note -``` - -But we need to add the `user` field into the mix. If the user is logged in to -Django through this browser, that information is automatically included in the -request... but where? Let's use the debugger to explore and find out. - -In `api.py`, `PersonalNoteSerializer`: - -```python - def create(self, validated_data): - import pdb; pdb.set_trace() # Start the debugger here - pass -``` - -Run this and use the debugger to the data present at this breakpoint. If you -dig into `self`, you will find eventually find a context with a request. As an -educated guess, using what we’ve previously learned about requests, it is fair -to hypothesize that a user is associated with the request. Try it out: - -```python -self.context['request'].user -``` - -Exit the debugger and add a new variable in `create` to store the user retrieved -from the location in `self` that we just discovered. Feed it in to `PersonalNote.objects.create` as an additional keyword argument: - -```python - def create(self, validated_data): - user = self.context['request'].user - note = PersonalNote.objects.create(user=user, **validated_data) - return note -``` - -This will add the needed data to the create method and allow the form to work. -Return to the `/api/notes/` page and test. - -You will receive a `201 Created` that may appear at first as if the data is -being overwritten. Return to the main list page to confirm that everything is -being saved. - -Debug as needed, then commit. - -## Filter Results by User - -Finally, we have one major problem remaining. Right now, any user can request -and see all of the notes that are in the database. We need to filter them so -that only the appropriate ones are returned. Return to `api.py`, -`PersonalNoteViewSet`. - -Change `queryset` to initialize with `Note.objects.none()`. This will create -the variable with an empty dictionary of the correct type. To make a decision -based on whether or not the user is anonymous, and return only the notes that -belong to the logged in user, we can override a method called -`get_queryset(self)`. - -Load the user into a variable. This class has access to the `request` directly, -so it can be found with `self.request.user`. - -```python - def get_queryset(self): - user = self.request.user -``` - -If `user.is_anonymous`, we can return an empty dictionary of notes. Otherwise, -we can use a filter to return only the correct ones with -`Note.objects.filter(user=user)`. - -```python - def get_queryset(self): - user = self.request.user - - if user.is_anonymous: - return PersonalNote.objects.none() - else: - return PersonalNote.objects.filter(user=user) -``` - -Test, debug, and commit. - -## Set up CORS - -In order to get your site to run well with a front-end, you might need to set up CORS: - -https://github.com/ottoyiu/django-cors-headers - -After installation (_follow the instructions at the link, above!_), setting: - -```python -CORS_ORIGIN_ALLOW_ALL = True -``` - -should be enough. - diff --git a/guides/day4.md b/guides/day4.md deleted file mode 100644 index a16adc2a4..000000000 --- a/guides/day4.md +++ /dev/null @@ -1,113 +0,0 @@ --------------------- -# Day 4: Token Auth for REST - -The Django Rest Framework also provides token authorization. We will use this -to allow other users to login and access the data specific to them. Information -can be found here: - -http://www.django-rest-framework.org/api-guide/authentication/#authentication - - -## Set up Token Authentication - -Open `settings.py`. - -To `INSTALLED_APPS`, add `rest_framework.authtoken`. - -If you need them elsewhere, immediately before the boilerplate for `REST_FRAMEWORK`, import `SessionAuthentication`, `BasicAuthentication`, and `TokenAuthentication` from `rest_framework.authentication` - -In `REST_FRAMEWORK`, add: - -```python - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'rest_framework.authentication.BasicAuthentication', - 'rest_framework.authentication.SessionAuthentication', - 'rest_framework.authentication.TokenAuthentication', - ), -``` - -## Set up the Route - -Next, we need to set up the route to authenticate users. In `urls.py`: - -Import `re_path` from `django.urls` and `views` from `rest_framework.authtoken` - -```python -from django.urls import path, include, re_path -from rest_framework.authtoken import views -``` - -Then add the endpoint in `urls.py` by adding the `api-token-auth/` route to -`urlpatterns`: - -```python - re_path(r'^api-token-auth/', views.obtain_auth_token) -``` - -The `^` is means "match the beginning of the string" in a regular expression. -The `re_path` function is just like `path`, except it interprets the endpoint as -a regex instead of a fixed string. - -Do a migration to set up the database. - -## Test the Endpoint - -We can test this on the bash command line with the [curl](https://curl.haxx.se/) -utility that you might already have installed. (Postman also works.) - -Mac/Linux: -``` -# The following makes a POST request with the given JSON payload: - -curl -X POST -H "Content-Type: application/json" -d '{"username":"admin", "password":"PASSWORD"}' http://127.0.0.1:8000/api-token-auth/ -``` - -Windows command prompt (or other platform if the above doesn't work): - -``` -# Windows needs some more double quotes and escaping of the payload - -curl -X POST -H "Content-Type: application/json" -d "{\"username\":\"admin\", \"password\":\"PASSWORD\"}" http://127.0.0.1:8000/api-token-auth/ -``` - -PowerShell has its own thing independent of `curl`: - -``` -Invoke-WebRequest http://localhost:8000/api-token-auth/ -Method Post -ContentType "application/json" -Body '{"username":"USER", "password":"PASS"}' -UseBasicParsing -``` - -If you get back a very large amount of html and other text, you have an error. -Scroll back up and google the error displayed just under your console command -for help troubleshooting. Many of the errors you can get here are easy to do, -common, and relatively easy to google for information on how to fix. - -You should get back one line with a token, for example: - -```json -{"token":"da51ccf5274050cd7332d184246d7d0775dc79e2"} -``` - -Your token will be different. Try it out with your token: - -``` -curl -v -H 'Authorization: Token da51ccf5274050cd7332d184246d7d0775dc79e2' http://127.0.0.1:8000/api/notes/ -``` - -Or, in PowerShell: - -``` -Invoke-WebRequest http://localhost:8000/api/notes/ -Headers @{"Authorization"="Token da51ccf5274050cd7332d184246d7d0775dc79e2"} -``` - -Note that the trailing `/` on the URL matters. You will get a 301 redirect if -you don’t add it here. - -When using Axios to send the request, set the header here: - -```javascript -axios.post('http://127.0.0.1:8000/api/notes/', data, { - headers: { - 'Authorization': 'Token da51ccf5274050cd7332d184246d7d0775dc79e2', - } -} -``` diff --git a/guides/trouble.md b/guides/trouble.md deleted file mode 100644 index 199556413..000000000 --- a/guides/trouble.md +++ /dev/null @@ -1,71 +0,0 @@ -# Common Errors and Troubleshooting - -## `pipenv` error `'module' object is not callable` - -If you're running `pipenv` and you get an error that ends like this: - -``` -"/usr/local/Cellar/pipenv/2018.7.1/libexec/lib/python3.7/site-packages/pipenv/vendor/requirementslib/models/requirements.py", line 704, in from_line -line, extras = _strip_extras(line) -TypeError: 'module' object is not callable -``` - -it might be time to upgrade `pipenv`. Make sure that `pipenv --version` is -outputting at least `2018.10.13`. - -## `pipenv` error `Found existing installation: [package name]` - -Add the following option to ignore existing installs: `--ignore-installed` - -``` -pip install pipenv --upgrade --ignore-installed -``` - -## `./manage.py` error `from exc` - -If you get this: - -``` - ) from exc - ^ -SyntaxError: invalid syntax -``` - -it usually means Python 2.x is running instead of Python 3. This, in turn, -usually means you've forgotten to `pipenv shell` into your virtual environment. - -Run `python --version` and make sure it says `3.`-something. - -## `./manage.py dbshell` not launching - -_Chief. Windows_ - -Make sure you have your SQLite3 package installed and in your path. - -A recommended way to install SQLite3 on Windows is [with -chocolatey](https://chocolatey.org/packages?q=sqlite). - -## `curl` not working from PowerShell - -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 - -```powershell -Remove-Item alias:curl -``` - -[Here are some instructions for removing the alias -permanently](https://superuser.com/questions/883914/how-do-i-permanently-remove-a-default-powershell-alias) - -## VS Code not recognizing Django imports properly - -Enable Django linting by adding the following to your VS Code workplace -settings: - -``` -"python.linting.pylingArgs":["--load-plugins","pylint_django"] -``` - -## Using PowerShell in VS Code terminal - -If you prefer it, [here are some instructions on setting it -up](https://code.visualstudio.com/docs/editor/integrated-terminal). diff --git a/manage.py b/manage.py new file mode 100755 index 000000000..ae1e94bc6 --- /dev/null +++ b/manage.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == '__main__': + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djorg.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) diff --git a/notes/__init__.py b/notes/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/notes/admin.py b/notes/admin.py new file mode 100644 index 000000000..f67a5a623 --- /dev/null +++ b/notes/admin.py @@ -0,0 +1,10 @@ +from django.contrib import admin + +from .models import Note, PersonalNote + +class NoteAdmin(admin.ModelAdmin): + readonly_fields=('created_at', 'last_modified') + +# Register your models here. +admin.site.register(Note, NoteAdmin) +admin.site.register(PersonalNote) \ No newline at end of file diff --git a/notes/api.py b/notes/api.py new file mode 100644 index 000000000..0dd13f733 --- /dev/null +++ b/notes/api.py @@ -0,0 +1,17 @@ +from rest_framework import serializers, viewsets +from .models import PersonalNote + +class PersonalNoteSerializer(serializers.HyperlinkedModelSerializer): + + class Meta: + model = PersonalNote + fields = ('title', 'content') + + def create(self, validated_data): + user = self.context['request'].user + note = PersonalNote.objects.create(user=user, **validated_data) + return note + +class PersonalNoteViewSet(viewsets.ModelViewSet): + serializer_class = PersonalNoteSerializer + queryset = PersonalNote.objects.all() \ No newline at end of file diff --git a/notes/apps.py b/notes/apps.py new file mode 100644 index 000000000..b6155aca3 --- /dev/null +++ b/notes/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class NotesConfig(AppConfig): + name = 'notes' diff --git a/notes/migrations/0001_initial.py b/notes/migrations/0001_initial.py new file mode 100644 index 000000000..f10cf1086 --- /dev/null +++ b/notes/migrations/0001_initial.py @@ -0,0 +1,36 @@ +# Generated by Django 2.1.4 on 2018-12-07 09:24 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Note', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('title', models.CharField(max_length=200)), + ('content', models.TextField(blank=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('last_modified', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='PersonalNote', + fields=[ + ('note_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='notes.Note')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + bases=('notes.note',), + ), + ] diff --git a/notes/migrations/__init__.py b/notes/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/notes/models.py b/notes/models.py new file mode 100644 index 000000000..444613b01 --- /dev/null +++ b/notes/models.py @@ -0,0 +1,15 @@ +from django.db import models +from uuid import uuid4 + +from django.contrib.auth.models import User + +class Note(models.Model): + id = models.UUIDField(primary_key=True, default=uuid4, editable=False) + title = models.CharField(max_length=200) + content = models.TextField(blank=True) + + created_at = models.DateTimeField(auto_now_add=True) + last_modified = models.DateTimeField(auto_now=True) + +class PersonalNote(Note): + user = models.ForeignKey(User, on_delete=models.CASCADE) diff --git a/notes/tests.py b/notes/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/notes/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/notes/views.py b/notes/views.py new file mode 100644 index 000000000..91ea44a21 --- /dev/null +++ b/notes/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..d46cf4a6b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +dj-database-url==0.5.0 +Django==2.1.4 +djangorestframework==3.9.0 +gunicorn==19.9.0 +psycopg2-binary==2.7.6.1 +python-decouple==3.1 +pytz==2018.7 +whitenoise==4.1.2