This repository is the back-end REST API which is used by my front-end project, League Hub.
You can view the live site here - League Hub
You can view the live API here - League Hub DRF API
You can view the front-end README.md here - League Hub Front-End README
You can view the back-end README.md here - League Hub Back-End README
- Objective
- Entity Relationship Diagram
- Database
- Models
- Testing
- Technologies Used
- Deployment To Heroku
- Cloning This Project
- Credits
- Acknowledgments
The objective of creating this API is to provide a fast, reliable and secure means of providing data to be used in my League Hub front-end project. I aim to create simple, intuitive and purposeful models that will be used to represent the data required for the project. All API endpoints will serve a specific purpose and will be tested thoroughly to prevent any attempt to create/edit/delete any data without the correct permissions.
To create the entity relationship diagram, I used a graph modelling tool Graph Models which I used on my fourth project. It shows the entire relationship between all models in the database. After following the steps required to install Graph Models, I then used dreampuf to present the data in a clear and professional way.
For this project, I implemented two databases.
The first one was the SQLite. This was used for the development side of the project and allows you to have a small, fast, self-contained SQL database engine.
The second database, which is a PostgreSQL database hosted by ElephantSQL was used for the production database.
To visually see the data within both databases, I used an excellent, lightweight tool called TablePlus which allows me to see all the data instantly and modify the data if needed through UI if I ever need to.
The champions model is designed to contain all the relevant information regarding a League of Legends champion.
Database Value | Field Type | Field Argument |
---|---|---|
owner | ForeignKey | User, on_delete=models.CASCADE |
created_at | DateTimeField | auto_now_add=True |
updated_at | DateTimeField | auto_now=True |
name | CharField | max_length=255 |
alias | CharField | max_length=255 |
champ_image | ImageField | upload_to="images/", default="../Ivern_0_iumwtm", blank=False |
lore | TextField | blank=False |
role | CharField | max_length=32, choices=role_choices, blank=False |
champ_class | CharField | max_length=32, choices=champ_class_choices, blank=False |
range | CharField | max_length=32, choices=range_choices, blank=False |
difficulty | CharField | max_length=32, choices=difficulty_choices, blank=False |
passive_ability | CharField | max_length=255 |
passive_ability_description | TextField | blank=False |
passive_ability_image | ImageField | upload_to="images/", default="../IvernW_muxhxj", blank=False |
ability_1 | CharField | max_length=255 |
ability_1_description | TextField | blank=False |
ability_1_image | ImageField | upload_to="images/", default="../IvernW_muxhxj", blank=False |
ability_2 | CharField | max_length=255 |
ability_2_description | TextField | blank=False |
ability_2_image | ImageField | upload_to="images/", default="../IvernW_muxhxj", blank=False |
ability_3 | CharField | max_length=255 |
ability_3_description | TextField | blank=False |
ability_3_image | ImageField | upload_to="images/", default="../IvernW_muxhxj", blank=False |
ultimate_ability | CharField | max_length=255 |
ultimate_ability_description | TextField | blank=False |
ultimate_ability_image | ImageField | upload_to="images/", default="../IvernW_muxhxj", blank=False |
role_choices = [
("top", "Top"),
("mid", "Mid"),
("jungle", "Jungle"),
("adc", "ADC"),
("support", "Support"),
]
champ_class_choices = [
("controller", "Controller"),
("fighter", "Fighter"),
("mage", "Mage"),
("marksman", "Marksman"),
("slayer", "Slayer"),
("tank", "Tank"),
("specialist", "Specialist"),
]
range_choices = [
("melee", "Melee"),
("ranged", "Ranged"),
]
difficulty_choices = [
("low", "Low"),
("moderate", "Moderate"),
("high", "High"),
]
The comment model allows the user to create a comment on a champion. If a comment is deleted, it is deleted from both the User and Champion models
Database Value | Field Type | Field Argument |
---|---|---|
owner | ForeignKey | User, on_delete=models.CASCADE |
champion | ForeignKey | Champion, on_delete=models.CASCADE |
created_at | DateTimeField | auto_now_add=True |
updated_at | DateTimeField | auto_now=True |
comment | TextField |
The profile model has a one-to-one relationship with the Django User model. This means that for every User that signs up to the website, there will be a corresponding Profile model which is used to define some additional values relative to the user. I've added a boolean field which will be used to determine if that user is a staff member or not. If this value is set to True then they will have staff permissions and will be able to perform all functionality of that of a staff member.
Database Value | Field Type | Field Argument |
---|---|---|
owner | OneToOneField | User, on_delete=models.CASCADE |
created_at | DateTimeField | auto_now_add=True |
updated_at | DateTimeField | auto_now=True |
first_name | CharField | max_length=255, blank=True |
last_name | CharField | max_length=255, blank=True |
is_staff | BooleanField | default=False |
avatar_image | ImageField | upload_to="images/", default="../Amumu_0_wzmdhw.jpg" |
The upvote model is a small model that is used to store the upvotes for a champion. An upvote is a foreign key of both the User and Champion model and if the User or the Champion is ever deleted then any Upvotes related to either the User or the Champion will be deleted.
Database Value | Field Type | Field Argument |
---|---|---|
owner | ForeignKey | User, on_delete=models.CASCADE |
champion | ForeignKey | Champion, on_delete=models.CASCADE, related_name="upvotes" |
created_at | DateTimeField | auto_now_add=True |
Application | Endpoint | Expected Result | Pass/Fail |
---|---|---|---|
Champions | champions/ | Return a list of all the champions in the database ordered by name | Pass |
Champions | champions/ | Searching for a champion by alphanumeric characters returns a list of matches | Pass |
Champions | champions/ | Applying a Top role filter returns only champions that have the role value of Top | Pass |
Champions | champions/ | Applying a Mid role filter returns only champions that have the role value of Mid | Pass |
Champions | champions/ | Applying a Jungle role filter returns only champions that have the role value of Jungle | Pass |
Champions | champions/ | Applying a ADC role filter returns only champions that have the role value of ADC | Pass |
Champions | champions/ | Applying a Support role filter returns only champions that have the role value of Support | Pass |
Champions | champions/int:pk/ | Returns a single champion with a correct ID and a list of all it's values | Pass |
Champions | champions/int:pk/edit | Returns a single champion with a correct ID and a list of all it's values and a staff member can edit the champion | Pass |
Champions | champions/int:pk/edit | Returns a single champion with a correct ID and a list of all it's values and the owner can edit the champion | Pass |
Champions | champions/int:pk/edit | Returns a single champion with a correct ID and a list of all it's values and a non-staff member can't edit the champion | Pass |
Champions | champions/int:pk/delete | Returns a single champion with a correct ID and a list of all it's values and a staff member can delete the champion | Pass |
Champions | champions/int:pk/delete | Returns a single champion with a correct ID and a list of all it's values and the owner can delete the champion | Pass |
Champions | champions/int:pk/delete | Returns a single champion with a correct ID and a list of all it's values and a non-staff member can't delete the champion | Pass |
Champions | champions/create | Return a list of all the champions a staff member can create a new champion | Pass |
Champions | champions/create | Return a list of all the champions a non-staff member can't create a champion | Pass |
Comments | comments/ | Return a list of all the comments in order of creation date | Pass |
Comments | comments/ | Applying a Champion filter will return all the comments relating to that specific champion only | Pass |
Comments | comments/int:pk/ | Returns a single comment with a correct ID and a list of all it's values | Pass |
Comments | comments/int:pk/ | Returns a single comment with a correct ID and a list of all it's values and the owner can edit the comment | Pass |
Comments | comments/int:pk/ | Returns a single comment with a correct ID and a list of all it's values and if the user isn't the owner of the comment, they can't edit it | Pass |
Comments | comments/int:pk/ | Returns a single comment with a correct ID and a list of all it's values and even if the user is a staff member, they can't edit it | Pass |
Comments | comments/delete/int:pk/ | Returns a single comment with a correct ID and a list of all it's values and if the owner of the comment can delete the comment | Pass |
Comments | comments/delete/int:pk/ | Returns a single comment with a correct ID and a list of all it's values and if the user is a staff member, they can delete the comment | Pass |
Comments | comments/delete/int:pk/ | Returns a single comment with a correct ID and a list of all it's values and if the user is not the owner of the comment, they can't delete it | Pass |
Profiles | profiles/ | Returns a list of all the profiles in the database ordered by creation date | Pass |
Profiles | profiles/int:pk/ | Returns a single profile with a correct ID and a list of all it's values and if the user isn't the owner of the profile, they can't edit it | Pass |
Profiles | profiles/int:pk/ | Returns a single profile with a correct ID and a list of all it's values and if the user is the owner of the profile, they can edit it | Pass |
Profiles | profiles/int:pk/ | Returns a single profile with a correct ID and a list of all it's values and if the user is a staff member, they can't edit it | Pass |
Upvotes | upvotes/ | Returns a list of all the current upvotes in the database | Pass |
Upvotes | upvotes/ | If a user is logged in, they can make a post request to create an upvote for a champion | Pass |
Upvotes | upvotes/ | If a user is logged in and they have already upvoted a champion and make a second post request to upvote it again, it fails with "Possible duplicate vote" | Pass |
Upvotes | upvotes/ | If a user is logged in and they have not already upvoted a champion and make a post request to upvote it, it succeeds and increases the upvote_count by 1 | Pass |
Upvotes | upvotes/ | If a user is logged out they are not able to upvote a champion | Pass |
Upvotes | upvotes/int:pk/ | Returns a single upvote with a correct ID and a list of all it's values | Pass |
Upvotes | upvotes/int:pk/ | If the user is the owner of the upvote, they can delete the upvote and it will delete it from the champion and decrease the upvote_count by 1 | Pass |
Upvotes | upvotes/int:pk/ | If the user is not the owner of the upvote, they are unable to delete the upvote | Pass |
Upvotes | upvotes/int:pk/ | If the user is not the owner of the upvote but is a staff member, they are unable to delete the upvote | Pass |
-
I've tested all the files through the CI PEP8 Linter and although I found a few errors, I have rectified these and now all files are passing with "All clear, no errors found"
manage.py
wsgi.py
views.py
urls.py
settings.py
serializer.py
permissions.py
upvotes - views.py
upvotes - urls.py
upvotes - serializers.py
upvotes - models.py
profiles - views.py
profiles - urls.py
profiles - serializers.py
profiles - models.py
comments - views.py
comments - urls.py
comments - serializers.py
comments - models.py
champions - views.py
champions - urls.py
champions - serializers.py
champions - models.py
-
When conducting manual tests for the API endpoints, I encountered a small bug which allowed users to delete comments that were not theirs. Although I had handled this issue in the front end, locking down the back-end is the most suitable solution to prevent any malicious attempt to delete comments by directly targeting the API.
The main problem was due to permissions. The comments/delete/int:pk/ endpoint was only checking if the user making the request was authenticated only.
To fix this, I created a new permission which would check if the user making the request was either a staff member or the owner of the comment
-
- As far as I'm aware, after extensive manual testing, I'm not aware of any unresolved bugs
- As far as I'm aware, after extensive manual testing, I'm not aware of any unresolved bugs
- Python - A programming language that lets you work quickly and integrate systems more effectively
- Django - Django is a high-level Python web framework that encourages rapid development and clean, pragmatic design.
- Django REST Framework - A powerful and flexible toolkit for building Web APIs
- asgiref - A standard for Python asynchronous web apps and servers to communicate with each other,
- black - A Python code formatter
- certifi - For validating the trustworthiness of SSL certificates while verifying the identity of TLS hosts
- charset-normalizer - A library that helps you read text from an unknown charset encoding
- cloudinary - Easily integrate your application with Cloudinary
- dj-database-url - Allows you to utilize the 12factor inspired DATABASE_URL environment variable to configure your Django application.
- dj-rest-auth - API endpoints for handling authentication securely in Django Rest Framework
- django-allauth - Integrated set of Django applications addressing authentication, registration, account management as well as 3rd party (social) account authentication
- django-cloudinary-storage - package that facilitates integration with Cloudinary by implementing Django Storage API
- django-cors-headers - Adds Cross-Origin Resource Sharing (CORS) headers to responses.
- django-extensions - Collection of global custom management extensions for the Django Framework.
- django-filter - Declaratively add dynamic QuerySet filtering from URL parameters.
- django-rest-auth - Provides a set of REST API endpoints for Authentication and Registration
- djangorestframework-simplejwt - JSON Web Token authentication plugin for the Django REST Framework.
- gunicorn - A Python WSGI HTTP Server for UNIX.
- idna - Support for the Internationalized Domain Names in Applications (IDNA) protocol
- mypy-extensions - Defines extensions to the standard “typing” module that are supported by the mypy type checker and the mypyc compiler.
- oauthlib - Implements the logic of OAuth1 or OAuth2 without assuming a specific HTTP request object or web framework.
- pathspec - Utility library for pattern matching of file paths
- Pillow - Adds image processing capabilities to your Python interpreter
- psycopg2 - PostgreSQL database adapter for Python
- pycodestyle - A tool to check your Python code against some of the style conventions in PEP 8.
- pydot - Library to generate .dot files which can be used to show ERD's
- PyJWT - Library for encoding and decoding JSON Web Tokens (JWT)
- pyparsing - Python parsing module
- pytz - Allows accurate and cross platform timezone calculations
- requests - Allows you to send HTTP/1.1 requests
- requests-oauthlib - OAuthlib authentication support for Requests
- six - A Python 2 and 3 compatibility library
- sqlparse - A non-validating SQL parser for Python. It provides support for parsing, splitting and formatting SQL statements.
- urllib3 - A powerful, user-friendly HTTP client for Python
- VSCode - Used to create and edit the website.
- GitHub - Used to host and deploy the website as well as manage the project.
- GitBash - Terminal used to push changes to the GitHub repository.
- Heroku - Used to deploy the website
- SQLite - An open-source, zero-configuration, self-contained, stand-alone, transaction relational database engine designed to be embedded into an application.
- ElephantSQL - Provides a browser tool for SQL queries where you can create, read, update and delete data directly from your web browser.
- Cloudinary - Used to host all static files .
- TablePlus - Used to view databases in a clean, simple way.
- Virutal Environment - Used to create a virtual environment
- Graph Models - Used to generate a .dot file for all apps and models
- dreampuf - Used to present the .dot file in the form of a database diagram
- CI PEP8 Linter - Used to check the Python code for any linting issues
The project was deployed to Heroku. The deployment process is as follows:
Firstly we need to create a new repository in GitHub where our project files will be located
- Navigate to GitHub
- Create a new repository with no template
Once you've created your new empty repository, we need to pull this repository down onto our local machine. Throughout the course I have used VSCode to create and manage my projects instead of GitPod so I will be demonstrating the process with VSCode.
- Copy either the HTTPS or SSH URL that has just been generated by GitHub
Now we need to open up a command prompt to pull this empty repository down onto our machine
- Open a CMD
- CD to a location you wish to store this project
- Now type git clone https://github.com/MikeR94/drf-api-deployment-process.git
- After the project has been pulled down onto your local machine, CD to the project and type code . to open the project with VSCode
Now it's time to install Django and some additional packages
- Install Django by typing pip install 'django<4'
- Create our new project by typing django-admin startproject drf_api_deployment_process .
- Install cloudinary storage by typing pip install django-cloudinary-storage
- Install Pillow by typing pip install Pillow
Now we need to add our newly installed apps to our settings.py file
Next, create a new env.py file and paste in the following code - remembering to change the "YOUR CLOUDINARY URL HERE" part to your API key
import os
os.environ["CLOUDINARY_URL"] = "YOUR CLOUDINARY_URL_HERE"
Back in our settings.py file we need to import our env.py file if it exists
import os
if os.path.exists("env.py"):
import env
Now we need to reference our new Cloudinary URL in settings.py
CLOUDINARY_STORAGE = {"CLOUDINARY_URL": os.environ.get("CLOUDINARY_URL")}
Now we need to define our MEDIA_URL and DEFAULT_FILE_STORAGE in settings.py
MEDIA_URL = "/media/"
DEFAULT_FILE_STORAGE = "cloudinary_storage.storage.MediaCloudinaryStorage"
Now it's time to start creating our applications. For my project I created 4 seperate applications
- champions
- comments
- profiles
- upvotes
Don't forget to add these applications to the INSTALLED_APPS variable in settings.py
After you have finished developing your application, you are ready to move onto the next deployment steps
First let's install JSON Web Token Authentication
- In the terminal type pip install dj-rest-auth
Add both rest framework’s auth token and django rest auth to INSTALLED APPS -
INSTALLED_APPS = [
"rest_framework.authtoken",
"dj_rest_auth",
]
Now add the urls to the urlpatterns list path('dj-rest-auth/', include('dj_rest_auth.urls'))
urlpatterns = [
path("dj-rest-auth/", include("dj_rest_auth.urls")),
]
Now migrate the database by typing python manage.py migrate
Next install Django All Auth with the following command - pip install 'dj-rest-auth[with_social]' and add the new application to the INSTALLED_APPS vairable in settings.py
INSTALLED_APPS = [
"django.contrib.sites",
"allauth",
"allauth.account",
"allauth.socialaccount",
"dj_rest_auth.registration",
]
Now add a SITE_ID variable in settings.py
SITE_ID = 1
Add the registration urls to the urlpatterns list
urlpatterns = [
path('dj-rest-auth/registration/', include('dj_rest_auth.registration.urls')),
]
Now it's time to Add JWT tokens functionality, in the terminal type in the following command pip install djangorestframework-simplejwt
In env.py add the following variable
os.environ['DEV'] = '1'
In settings.py set the DEBUG value equal to the DEV variable you just set in env.py
DEBUG = 'DEV' in os.environ
Next, still in settings.py add the following code differentiate between development and production modes and also set the pagination and date time format
REST_PAGINATION = "rest_framework.pagination.PageNumberPagination"
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
(
"rest_framework.authentication.SessionAuthentication"
if "DEV" in os.environ
else "dj_rest_auth.jwt_auth.JWTCookieAuthentication"
)
],
"DEFAULT_PAGINATION_CLASS": REST_PAGINATION,
"PAGE_SIZE": 100,
"DATETIME_FORMAT": "%d %b %Y",
}
And now add the following code to settings.py to enable token authentication, cookie declaration and to also ensure that the tokens are sent over HTTPS only
REST_USE_JWT = True
JWT_AUTH_SECURE = True
JWT_AUTH_COOKIE = "my-app-auth"
JWT_AUTH_REFRESH_COOKIE = "my-refresh-token"
JWT_AUTH_SAMESITE = "None"
Now we can create a new root route which will act as a welcome screen to anyone who visits the root route of our API
- Create a new views.py file in your main project folder (drf_api_league_hub) and add the following code
from rest_framework.decorators import api_view
from rest_framework.response import Response
@api_view()
def root_route(request):
"""
Function to return a welcome message
upon loading the API
"""
return Response({"message": "Welcome to the League Hub DRF API!"})
Add that new route to the urlpatterns list in the main urls.py file
from .views import root_route
path("", root_route),
Now it's time to create a new production database with ElephantSQL
- Log into ElephantSQL
- Click Create New Instance
- Give your plan a name
- Select *Tiny Turtle (Free) plan
- Click Select Region and pick a data center near you
- Click Review, double check the information you provided, once happy, click Create Instance
We need to get the new database URL from the ElephantSQL dashboard.
Return to the dashboard, find your newly created plan and copy the URL
Back in our local project, in eny.py we can add our copied production database URL and store it in a new variable which can be referenced without exposing the value to unwanted eyes
os.environ.setdefault(
"DATABASE_URL", "YOUR DB URL HERE",
)
In the terminal, type in pip install dj_database_url
After that has installed, we need to import it into our main settings.py file
import dj_database_url
Now we can seperate the development and production environment databases with the following code
if "DEV" in os.environ:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
else:
DATABASES = {"default": dj_database_url.parse(
os.environ.get("DATABASE_URL"))}
print("Connected to live database")
Next we need to install gunicorn. In the terminal, type in the following
- pip install gunicorn
Now created a Procfile at the root level in your project
Within that newly created Procfile, add the following code to let Heroku know how to run the project
release: python manage.py makemigrations && python manage.py migrate
web: gunicorn drf_api_league_hub.wsgi
Back in settings.py, we need to tell the project which hosts to allow
ALLOWED_HOSTS = [
os.environ.get("ALLOWED_HOST"),
"127.0.0.1",
]
Now it's time to install CORS
In the terminal, type in pip install django-cors-headers and then add it to the INSTALLED_APPS
INSTALLED_APPS = [
'corsheaders',
]
Add it to the MIDDLEWARE list - it is important that it is placed at the top of the list
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware",
]
Now we need to set the ALLOWED_ORIGINS for the network requests made to the server. In settings.py file, add the below
if "CLIENT_ORIGIN" in os.environ:
CORS_ALLOWED_ORIGINS = [
os.environ.get("CLIENT_ORIGIN"),
]
if "CLIENT_ORIGIN_DEV" in os.environ:
CORS_ALLOWED_ORIGINS.append(os.environ.get("CLIENT_ORIGIN_DEV"))
CORS_ALLOW_CREDENTIALS = True
JWT_AUTH_SAMESITE = 'None'
Now we need to replace the SECRET_KEY variable in settings.py to reference the secret key that we will create in env.py
In env.py create a new variable called SECRET_KEY and give it a value
os.environ.setdefault("SECRET_KEY", "YOUR SECRET KEY HERE")
Now back in settings.py, change the SECRET_KEY value to point to the SECRET_KEY you just created in env.py
SECRET_KEY = os.getenv("SECRET_KEY")
Update the requirements file by typing in the command in the terminal pip freeze > requirements.txt
- git add
- git commit -m "{message here}
- git push
Now it's time to deploy our new project live for everyone to see on Heroku
- Navigate to Heroku
- Click New app
- Fill in the relevant information
- Click Create app once you are happy
Once you've created your new Heroku application, we need to add some config variables. Click the settings tab and then click Reveal Config Vars
Make sure to add the following config variables
- ALLOWED_HOST - This is the URL of your deployed project (without the https)
- CLIENT_ORIGIN - This is the URL of your deployed front-end project
- CLIENT_ORIGIN_DEV - This is the URL when developing locally
- CLOUDINARY_URL - This is your Cloudinary API key
- DATABASE_URL - This is your production database URL
- DISABLE_COLLECTSTATIC - This will be removed before submission
- SECRET_KEY - This is the secret key you have created
Now we need to link our Heroku application with our GitHub project
- Click the Deploy tab
- Choose GitHub
- Search for your repository
- Once found, click Connect
Finally, we can deploy our connected projected for everyone to see.
You can either choose Enable Automatic Deploys or Deploy Branch. I chose to deploy my application manually when I was ready instead of automatic deployments
- Click Deploy Branch and wait for it to build
This project was created and inspired by following the Code Institute DRF-API walkthrough and has been modified to meet the League Hub's demands
I spent some time looking through the official Django REST Framework Documentation to help me further understand some concepts, especially the permissions one where I needed to create a new IsStaffOrOwnerOrReadOnly permission
All the champion data stored in the database has been sourced from the official League of Legends website
I have thoroughly enjoyed developing this project and although I found React to be challenging learning curve, after much perseverance I feel like I have a good baseline knowledge when it comes to developing applications that use an advanced front-end framework like React that talk to a back-end API developed using the Django REST Framework. Unlike my other projects which I was able to work on full time, I had successfully landed a software developer job and I only had time to work on this project in my spare time whilst trying to have a balanced personal life. This was extremely challenging for myself, and I am very proud to have managed to finally submit my fifth and final project with Code Institute
I would like to thank my mentor Marcel, my educator Luke Walters, my brother Jack Ralph, my partner Beth, the Slack community, and all at the Code Institute for their help and support.
It has been an incredible journey and I’m extremely excited to see where this adventure will take me.
Thank you so much for a fantastic experience Code Institute!
Mike Ralph 2023.