With help from https://medium.com/swlh/build-your-first-rest-api-with-django-rest-framework-e394e39a482c
Make sure this is in your bash profile - you can write it in Atom and save it in your home directory's bash profile
export VIRTUALENVWRAPPER_PYTHON=/usr/local/bin/python3 export WORKON_HOME=$HOME/.virtualenvs export PROJECT_HOME=$HOME/Code source /usr/local/bin/virtualenvwrapper.sh
It might be a .zshrc profile instead. This part is a little confusing and you may need some help with it if you're on a new machine.
Make sure you're not in a virtual environment already. mkvirtualenv project-name
. You can do this from your project folder.
Once you've done that, make sure you're in the right directory! Now, type setvirtualenvproject
and it will set your directory as the project for whatever virtual directory you just made.
When you want to work on your virtual env, type workon virtualenv-name
and it will plop you into your directory and activate the environment in your terminal.
When you're done, click deactivate
.
//question for Kit - WHY do we make a virtual env again?
In the virtual env, type pip install django
Once that has been installed, type django-admin startproject project_name .
The . will make sure it's made right there in the directory without making a new one.
python manage.py startapp app_name_api
Use this to make other apps you're adding. Each app in a project needs to know about each other.
In settings.py of the project folder, which is the only one that will contain this file you need to put the apps you made in the folders below in the INSTALLED_APPS list
app_name.apps.App_NameConfig
The classname after apps. is in admin.py in the app's folder tree.
From anywhere in your terminal, type createdb database_name
. Postgres managed to claim this command word before anyone else did.
Now, make a requirements.txt file to keep track of your requirements in the repo root. You NEED to name your requirements what you'd call after pip install
- this file will be used to do all of your installations.
To that file, add:
django psycopg2-binary
Once your requirements are in the requirements folder, you can do pip install -r requirements.txt
With Django, we need to migrate before we make our models because Django comes with some pre-made "migrations" - tables and column names. 'Users' is one, for instance. You can't make a superuser until you've done the migration!
Before we migrate for the first time, we need to set up PostGres in the project's settings. Scroll down to DATABASES.
Set its engine as django.db.backends.postgresql
. The name in for a postgres DB is just the DB name - you can see that it comes filled in with a sqlite db and is set up a little differently.
Type python manage.py migrate
to migrate you db.
Create a superuser: python manage.py createsuperuser
Verify that everything is working: python manage.py runserver
and then navigate to localhost:8000/admin
Open up the models.py file in your api app. Make sure to import django if it's not there already by writing from django.db import models
at the top.
Each table is going to be a class. Character fields in Django do need character limits (even if that's not a thing in PostGres), so it might behoove you to write BIG_STRING = 60000
at the top and set your character limits to that.
Here is an example of a table, writted as a class:
`class CharacterDescription(models.Model):
body = models.CharField(max_length=BIG_STRING)
quiz = models.ForeignKey(Quiz, on_delete=models.CASCADE)
character = models.ForeignKey(Character, on_delete=models.CASCADE)
class Meta:
ordering = ['body']
def __str__(self):
return self.body`
If a column depends on another table, make sure it's written after the table it depends on! Otherwise your tables will be very confused when you try to migrate.
There are many different types of fields your columns can be - you should probably check the Django docs if you want to learn them all.
Here are a few not connected to any other models:
models.CharField(max_length=BIG_STRING)
Character fields are strings and do need a character limit.
models.ImageField()
Image fields contain all sorts of info about an image that you can upload in the admin. If you need to access the url, you'll need to do field_name.url.
models.BooleanField(default=True)
Boolean fields will need a default
Here is a one-to-many type field:
models.ForeignKey(Location, on_delete=models.CASCADE)
You specify that it's a foreign key, and the arguments that function takes are the field it's connected to and how to handle the delete.
Here is a many-to-many field without an explicit through table:
members = models.ManyToManyField(Character, related_name="house")
Django makes a through table under the hood. Sometimes, you will need a related name for a field if more than one field uses the same model.
Aaaand...here is how to make a many-to-many field that references itself and uses and explicit through table:
connections = models.ManyToManyField("self", through="Connection")
The many to many field method on models takes the name of the model that it's connected to normally, but in this case you can just put "self." If there is an explicit through table, add the name of that model as well.
This one needs a special through table because of the enemy/ally booleans that exist on each character, so it's more than just a way to connect two datasets. It connects and defines them.
Here is the code for doing this.
class Character(models.Model):
name = models.CharField(max_length=BIG_STRING)
connections = models.ManyToManyField("self", through="Connection")
image = models.ImageField()
class Meta:
ordering = ['name']
def __str__(self):
return self.name
def as_JSON(self):
return {
'name': self.name,
'allies': [
connection.to_character.name
for connection
in Connection.objects.filter(from_character=self, is_ally=True)
],
'enemies': [
connection.to_character.name
for connection
in Connection.objects.filter(from_character=self, is_enemy=True)
],
'image': self.image.url
}
class Connection(models.Model):
from_character = models.ForeignKey(Character, on_delete=models.CASCADE, related_name="outgoing_connections")
to_character = models.ForeignKey(Character, on_delete=models.CASCADE, related_name="incoming_connections")
is_ally = models.BooleanField(default=True)
is_enemy = models.BooleanField(default=False)
You may notice that there is a method in the model that changes the format off the data to a JSON object. There isn't a JSON stringify method in Python - you just kind of have to build the object.
You'll notice that a list comprehension is used for each connection. (Comprehensions are not methods, they are part of Python's syntax. You can use them on dicts, lists and sets). There isn't really anything like them in JS.
'allies': [
connection.to_character.name
for connection
in Connection.objects.filter(from_character=self, is_ally=True)
],
This is a way of mapping over lists in Python. It's saying that in a filtered list of items in the Connections column, for each connection, spit out the name of the object in the to_character field.
Make sure to register you models with the admin site if you want them too. Do this in your api app's admin.py
from django.contrib import admin
from .models import Character
admin.site.register(Character)
Start by making a urls.py file in your api app. This will basically be your router.
You will need to do these imports:
from django.urls import path
from . import views
Next, you set urlpatterns (magic from Django, django.urls knows what this is) to equal a list of paths, whose first arguments are the URL paths you will be using. You can name these yourself. The second argument is the view, called off of views.
urlpatterns = [
path('characters/', views.character_index),
path('characters/<int:character_id>', views.single_character),
path('locations/', views.location_index),
]
Now, in views.py, set up your views. Make sure you name them to match the views you're matching with your paths!
These are the imports you'll need:
from django.shortcuts import get_object_or_404
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
import json
from .models import Character, Connection
get_object_or_404 is a handy little method that basically bundles your try
and except
functionality so that you don't have to explictly make your app throw and error if you can't find what you're looking for.
HttpResponse is essentially your response object - unlike in Express, you don't feed it as an argument to your function. It's its own thing - it's argument is what you want the response to be.
csrf_exempt - ah. This makes it go. There's some security stuff...probs aren't going to want to do this at work.
Here are some views - basically, you use if statements to tell each endpoint how to react to each method, and then you build your response by calling and formatting things off of your models.
def _find_connection(x):
return Character.objects.get(name=x)
@csrf_exempt
def character_index(request):
if request.method == "GET":
character_list = Character.objects.all()
output = json.dumps([c.as_JSON() for c in character_list])
return HttpResponse(output)
if request.method == "POST":
body = json.loads(request.body)
new_character = Character.objects.create(name=body["name"])
new_allies = [Connection.objects.create(from_character=new_character, to_character=_find_connection(a), is_ally=True) for a in body["allies"]]
new_enemies = [Connection.objects.create(from_character=new_character, to_character=_find_connection(e), is_enemy=True) for e in body["enemies"]]
output = json.dumps(new_character.as_JSON())
return HttpResponse(output)
return HttpResponse(status=405)
@csrf_exempt
def single_character(request, character_id):
# try:
# chosen_character = Character.objects.get(id=character_id)
# except Character.DoesNotExist:
# return HttpResponse(status=404)
if request.method == "GET":
chosen_character = get_object_or_404(Character, id=character_id)
output = json.dumps(chosen_character.as_JSON())
return HttpResponse(output)
if request.method == "PUT":
chosen_character = get_object_or_404(Character, id=character_id)
body = json.loads(request.body)
chosen_character.name = body["name"]
# chosen_character.allies = body["allies"]
# chosen_character.enemies = body["enemies"]
chosen_character.save()
new_allies = [Connection.objects.create(from_character=chosen_character, to_character=_find_connection(a), is_ally=True) for a in body["allies"]]
new_enemies = [Connection.objects.create(from_character=chosen_character, to_character=_find_connection(e), is_enemy=True) for e in body["enemies"]]
output = json.dumps(chosen_character.as_JSON())
return HttpResponse(output)
if request.method == "DELETE":
chosen_character = get_object_or_404(Character, id=character_id)
chosen_character.delete()
return HttpResponse(status=204)
return HttpResponse(status=405)