Skip to content

Instantly share code, notes, and snippets.

@spaceplesiosaur
Last active May 29, 2020 05:16
Show Gist options
  • Save spaceplesiosaur/7251284099a933d0e6e329a6c8ffccec to your computer and use it in GitHub Desktop.
Save spaceplesiosaur/7251284099a933d0e6e329a6c8ffccec to your computer and use it in GitHub Desktop.

Make a Python and Django app

With help from https://medium.com/swlh/build-your-first-rest-api-with-django-rest-framework-e394e39a482c

Make sure python is installed and virtualenvwrapper is installed.

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 a new virtual environment

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?

Install Django and make/start project

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.

Make the API app and add it to the project

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.

Make DB and install stuff

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

Migrate the DB

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

Make your models

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)

Format your data

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)

Make your views

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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment