Project Code:
All the code contained herein can be found at: E_WalletZ. Please feel free to clone, pull,star or use it as you like. Also, don't forget to follow me for more projects on React, React-Native, Python(Django) and Go-lang. Happy Learning.
Disclaimer !!.
Please note that code contained in this project is nowhere near close to being production grade.This project is easy enough for mid-level developers, for beginners you will need to make chat-GPT your friend(i Assume you have a pretty good understanding of python as a language and Django as a framework),if you don't understand a block of code just paste it on chat-GPT and it will give you an in-depth explanation. I hope it will be fun project to build and learn with.
Prelude
In today's digital world, finance in its essence has entirely gone online. By the essence of finance, i mean the holding, moving and exchange of money for goods and services . Based on these macro-events, we have seen a push by countless startups to bank the unbanked, and the easiest way to do this is to use the smartphones that have found their way into the hands of most of earths population. Simply ask the people to register for an application account and then they can use this account to send, receive money, purchase goods and services, access overdraft based on expenditures, access insurance etc.
In this article i want us to build simple wallets to hold cash or tokens,The wallets will allow for depositing of cash, withdrawal of cash and transfer of cash between wallets of different persons.
Technologies Used:
Django Framework
This is a high-level python framework that can be used to build almost any type of website. We will be using it to build our API.
Graphql
This is an open source API query language, it majorly describes how a client should ask for information through an API. We will be using graphl to present our back-end data for clients. I might have as well chosen to use the REST framework, but to avoid over-fetching and under-fetching and make it easier for front-end integration i chose graphql. BTW, let me know in the comments if you would like me to make a follow up tutorial building a React-Native app to consume the API.
AWS S3(Simple Storage Service)
Amazon web services is one of the most popular cloud computing provider. We will be using one of it most popular services called s3. s3 stands for simple storage service and its an object storage service. We will use it to store the static and media files for Django and Graphql.
JWT(Json Web Token)
We will be using this for user authentication. Still a lightweight authentication method but it will do for this case. Please remember that for production level projects the authentication procedures have to be more stringent, JWT wont do. JWT object will carry our users information.
Docker Containers
We will containerize our service and will also run the PostgreSQL database in a container. A docker container image is a lightweight, standalone executable package of software that includes everything needed to run our application.
Postgres Database
This is a relational database. It supports both relational(SQL) and non-relational(JSON) functions. Postgres is better than MySQL in terms of read-write operations, dealing with massive datasets and executing complicated queries.
Pre-requisites
Setting up the developer environment
Operating system
I am assuming, you will be using a UNIX based OS for development, in my case i am using Ubuntu 22.04.3 LTS. If you are using windows please install WSL(Windows Subsystem for Linux), then install Ubuntu.Follow the tutorial here.
virtualenv
To set up our environment we are going to create a virtual environment, what this does is it creates a separate environment from the systems' environment, where you can add all you packages and libraries without mixing those up with the rest of the systems environment installed packages, it creates sort of a "sandbox" environment, so lets go ahead and create one:
check if you have virtualenv installed on your system.
virtualenv --version
"cd" into your folder where you do your development.
Make a directory called WalletZ, and then "cd" into this new directory.Now in this WalletZ directory, lets create a virtual environment as shown below:
mykmyk@mykmyk-Lenovo-G50-30:~/data/Development/Django/EcommerceProjects/WalletZ$ virtualenv venv
created virtual environment CPython3.10.12.final.0-64 in 889ms
creator CPython3Posix(dest=/home/mykmyk/data/Development/Django/EcommerceProjects/WalletZ/venv, clear=False, no_vcs_ignore=False, global=False)
seeder FromAppData(download=False, pip=bundle, setuptools=bundle, wheel=bundle, via=copy, app_data_dir=/home/mykmyk/.local/share/virtualenv)
added seed packages: pip==23.0.1, setuptools==67.6.0, wheel==0.40.0
activators BashActivator,CShellActivator,FishActivator,NushellActivator,PowerShellActivator,PythonActivator
mykmyk@mykmyk-Lenovo-G50-30:~/data/Development/Django/EcommerceProjects/WalletZ$ ls -la
total 12
drwxrwxr-x 3 mykmyk mykmyk 4096 Okt 22 21:46 .
drwxrwxr-x 5 mykmyk mykmyk 4096 Okt 22 20:58 ..
drwxrwxr-x 4 mykmyk mykmyk 4096 Okt 22 21:46 venv
mykmyk@mykmyk-Lenovo-G50-30:~/data/Development/Django/EcommerceProjects/WalletZ$
lets then activate that environment as shown below:
mykmyk@mykmyk-Lenovo-G50-30:~/data/Development/Django/EcommerceProjects/WalletZ$ source venv/bin/activate
(venv) mykmyk@mykmyk-Lenovo-G50-30:~/data/Development/Django/EcommerceProjects/WalletZ$ ls -la
total 12
drwxrwxr-x 3 mykmyk mykmyk 4096 Okt 22 21:46 .
drwxrwxr-x 5 mykmyk mykmyk 4096 Okt 22 20:58 ..
drwxrwxr-x 4 mykmyk mykmyk 4096 Okt 22 21:46 venv
(venv) mykmyk@mykmyk-Lenovo-G50-30:~/data/Development/Django/EcommerceProjects/WalletZ$
Now that we have activated our virtualenv, lets install the Django framework version 3.1.5.We are using pip the package manager for python to do this.
(venv) mykmyk@mykmyk-Lenovo-G50-30:~/data/Development/Django/EcommerceProjects/WalletZ$ pip install Django==3.1.5
We then create out Django Project "E-WalletZ" to house our apps, as shown in the code below:
(venv) mykmyk@mykmyk-Lenovo-G50-30:~/data/Development/Django/EcommerceProjects/WalletZ$ django-admin startproject E_WalletZ
(venv) mykmyk@mykmyk-Lenovo-G50-30:~/data/Development/Django/EcommerceProjects/WalletZ$ ls
E_WalletZ venv
Enter into the new E_WalletZ directory, we want to create our apps:
(venv) mykmyk@mykmyk-Lenovo-G50-30:~/data/Development/Django/EcommerceProjects/WalletZ/E_WalletZ$ django-admin startapp Wallet
(venv) mykmyk@mykmyk-Lenovo-G50-30:~/data/Development/Django/EcommerceProjects/WalletZ/E_WalletZ$ ls -la
total 20
drwxrwxr-x 4 mykmyk mykmyk 4096 Okt 22 22:54 .
drwxrwxr-x 4 mykmyk mykmyk 4096 Okt 22 22:23 ..
drwxrwxr-x 2 mykmyk mykmyk 4096 Okt 22 22:23 E_WalletZ
-rwxrwxr-x 1 mykmyk mykmyk 665 Okt 22 22:23 manage.py
drwxrwxr-x 3 mykmyk mykmyk 4096 Okt 22 22:54 Wallet
(venv) mykmyk@mykmyk-Lenovo-G50-30:~/data/Development/Django/EcommerceProjects/WalletZ/E_WalletZ$ django-admin startapp Transanctions
(venv) mykmyk@mykmyk-Lenovo-G50-30:~/data/Development/Django/EcommerceProjects/WalletZ/E_WalletZ$ ls
E_WalletZ manage.py Transanctions Wallet
(venv) mykmyk@mykmyk-Lenovo-G50-30:~/data/Development/Django/EcommerceProjects/WalletZ/E_WalletZ$ ls -la
total 24
drwxrwxr-x 5 mykmyk mykmyk 4096 Okt 22 22:54 .
drwxrwxr-x 4 mykmyk mykmyk 4096 Okt 22 22:23 ..
drwxrwxr-x 2 mykmyk mykmyk 4096 Okt 22 22:23 E_WalletZ
-rwxrwxr-x 1 mykmyk mykmyk 665 Okt 22 22:23 manage.py
drwxrwxr-x 3 mykmyk mykmyk 4096 Okt 22 22:55 Transanctions
drwxrwxr-x 3 mykmyk mykmyk 4096 Okt 22 22:54 Wallet
(venv) mykmyk@mykmyk-Lenovo-G50-30:~/data/Development/Django/EcommerceProjects/WalletZ/E_WalletZ$ django-admin startapp user_controller
(venv) mykmyk@mykmyk-Lenovo-G50-30:~/data/Development/Django/EcommerceProjects/WalletZ/E_WalletZ$ ls -la
total 28
drwxrwxr-x 6 mykmyk mykmyk 4096 Okt 22 22:55 .
drwxrwxr-x 4 mykmyk mykmyk 4096 Okt 22 22:23 ..
drwxrwxr-x 2 mykmyk mykmyk 4096 Okt 22 22:23 E_WalletZ
-rwxrwxr-x 1 mykmyk mykmyk 665 Okt 22 22:23 manage.py
drwxrwxr-x 3 mykmyk mykmyk 4096 Okt 22 22:55 Transanctions
drwxrwxr-x 3 mykmyk mykmyk 4096 Okt 22 22:55 user_controller
drwxrwxr-x 3 mykmyk mykmyk 4096 Okt 22 22:54 Wallet
Move into the E_Walletz directory that houses the files "asgi.py", "init.py", "settings.py","urls.py", "wsgi.py".
A little explanation of the files:
asgi.py
This is the asynchronous server gateway interface,it defines an interface between asynchronous python web servers and applications and also supports all features provided by WSGI(web server gateway interface).
wsgi.py
This file defines a communication interface between a web server and a python web application. Since its synchronous its less suitable for handling long-lived connections.
settings.py
This file houses all the configurations to the project we are working on. It contains database configurations, middlewares(for Django and Graphql), security settings. It provides with a centralized project configuration.
urls.py
Its in this file that we sew together specific urls to their respective views or handlers, what is known as URL routing.In this file we define how incoming request are mapped to their respective views.
Now open the "settings.py" file, in that we import the OS package and if you look at the "INSTALLED_APPS" list we add all the apps we just created "user_controller", "Transanctions", "Wallet".
"settings.py"
from pathlib import Path
import os
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-n18use^q@nwz&)s#os&zxlhy=3d57ys1wubfwi036e4h)7rol8'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'user_controller',
'Transanctions',
'Wallet'
]
MIDDLEWARE = [
'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 = 'E_WalletZ.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 = 'E_WalletZ.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/4.2/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/4.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
AWS CONNECTION
Storing our Static Files In An AWS s3 Bucket
We now want to push static files related to our Django app to the AWS S3 bucket, lets first head to AWS , if you have an account that is perfect, if not then here is a link to help you through the process of creating one here. After you have created an account ,generate access keys for that account(this will be used by Django to access the AWS resources, in this case the s3 bucket). Here is a link to guide you in creating access keys.
Creating an s3 Bucket:
On the AWS console search for s3, which stand for simple storage service, its a service that enables for object storage of virtually any size. We want to create a new s3 Bucket called "e-walletz" as shown below:
choose the region as "Europe(London) eu-west-2", on the Object Ownership choose "ACLs enabled " as shown below :
Leave all other settings as they are and proceed to create the "e-walletz" bucket, after it has completed creating enter into the bucket and choose "create a folder" button, create a folder named "static" as shown below:
One last step ......
On the "e-walletz" page, click on "permissions" tab and scroll down to bucket policy, click on edit :
Then Click on Policy generator :
We want to allow access to our Bucket and its children, allowing essentially all actions for our principal .(PS: to be safe you can restrict the actions to uploads and downloads only)
generate the access policy and then copy paste it onto the text field that is supposed to hold our Bucket policy.
Phew !.... back to Django now..
Create a ".env" file at the project root level as below:
The contents of this .env file should be as follows, remember this file holds our environment variables ,and we will plug those into the "settings.py" using decouple tool.
".env"
DEBUG=True
DB_NAME=E_WalletZ_user
DB_USER=E_WalletZ_user
DB_PASSWORD=E_WalletZ_password
DB_HOST= 'here you will put the ip address of your host machine'
DB_PORT=5432
AWS_STORAGE_BUCKET_NAME='e-walletz'
AWS_S3_ACCESS_KEY_ID='input the access key ID we generated earlier'
AWS_S3_SECRET_ACCESS_KEY='input the secret access key generated earlier'
AWS_HOST_REGION='eu-west-2'
S3_BUCKET_URL='https://e-walletz.s3.eu-west-2.amazonaws.com/static/'
You will notice that i have indicated that for "DB_HOST" you will put your host machine IP address, this is a functionality that will be scripted when doing automated deployment(CI/CD). To find machine IP use the following command:
ifconfig | grep "172."
Now head to the "settings.py" file, located here :
Install decouple package using the command:
The decouple package helps us strictly separate the settings parameters from the source code. As you go on you will notice i haven't strictly applied myself to this.
pip install python-decouple==3.4
pip freeze > requirements.txt
The contents of the file should look like below, notice we import config from decouple at the top, then we use it to plug in all the environment variables we just set in our ".env" file :
"settings.py"
from pathlib import Path
import os
from decouple import config
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-n18use^q@nwz&)s#os&zxlhy=3d57ys1wubfwi036e4h)7rol8'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ["*"]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'user_controller',
'Transanctions',
'Wallet'
]
MIDDLEWARE = [
'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 = 'E_WalletZ.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 = 'E_WalletZ.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/4.2/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/4.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
#AWS CONFIG
# AWS CONFIG
AWS_STATIC_LOCATION = 'static'
S3_BUCKET_URL = config('S3_BUCKET_URL')
STATIC_ROOT = 'staticfiles'
AWS_ACCESS_KEY_ID = config('AWS_S3_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = config('AWS_S3_SECRET_ACCESS_KEY')
AWS_STORAGE_BUCKET_NAME = config('AWS_STORAGE_BUCKET_NAME')
AWS_HOST_REGION = config('AWS_HOST_REGION')
AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
AWS_DEFAULT_ACL = None
AWS_LOCATION = 'static'
MEDIA_URL = 'media/'
AWS_QUERYSTRING_AUTH = False
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),
]
STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{AWS_LOCATION}/'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
Notice all the AWS Configuration we have done above, these will help us "talk" to the s3 bucket we just created.
Install s3transfer(a python library for managing AWS s3 transfers) and django-storages(provides a variety of storage backends in a single library) packages as shown below:
pip install s3transfer==0.3.7
pip install django-storages==1.11.1
pip freeze > requirements.txt
head back to the folder that held the "settings.py" file and create a new file "storage_backends.py"
Add the following contents into the file
from storages.backends.s3boto3 import S3Boto3Storage
from django.conf import settings
class MediaStorage(S3Boto3Storage):
location = settings.AWS_STATIC_LOCATION
default_acl = 'public-read'
file_overwrite = False
The code above in its totality allow us to use AWS s3 object store to store and serve media files for our application.So we want to store and serve static and media files from AWS s3 not locally to do this we are sub-classing S3BotoStorage class that we have imported from storages.backends.s3boto3, we then define our custom class MediaStorage and in this we specify settings for location of storage, default access control list, and file overwrite settings.
Now open the "settings.py" and reference the local Media storage we just created : the file should now look as below with the new additions;
"settings.py"
from pathlib import Path
import os
from decouple import config
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-n18use^q@nwz&)s#os&zxlhy=3d57ys1wubfwi036e4h)7rol8'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ["*"]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'user_controller',
'Transanctions',
'Wallet'
]
MIDDLEWARE = [
'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 = 'E_WalletZ.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 = 'E_WalletZ.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/4.2/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/4.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
#AWS CONFIG
# AWS CONFIG
AWS_STATIC_LOCATION = 'static'
S3_BUCKET_URL = config('S3_BUCKET_URL')
STATIC_ROOT = 'staticfiles'
AWS_ACCESS_KEY_ID = config('AWS_S3_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = config('AWS_S3_SECRET_ACCESS_KEY')
AWS_STORAGE_BUCKET_NAME = config('AWS_STORAGE_BUCKET_NAME')
AWS_HOST_REGION = config('AWS_HOST_REGION')
AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
AWS_DEFAULT_ACL = None
AWS_LOCATION = 'static'
MEDIA_URL = 'media/'
AWS_QUERYSTRING_AUTH = False
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),
]
STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{AWS_LOCATION}/'
DEFAULT_FILE_STORAGE = 'E_WalletZ.storage_backends.MediaStorage'
STATICFILES_STORAGE = 'E_WalletZ.storage_backends.MediaStorage'
AWS_S3_OBJECT_PARAMETERS = {
'CacheControl': 'max-age=86400',
}
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
Setting Up Our Docker Containers.
We want our service the E_WalletZ API and the Postgres Database that we will be using to persist our data to be run inside docker containers;
In our project root directory ; create a file "Dockerfile":
The contents of that file should look like:
"Dockerfile"
FROM python:3.7
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
RUN mkdir /E_WalletZ
WORKDIR /E_WalletZ
COPY . /E_WalletZ/
RUN pip install -r requirements.txt
This file is used to build the image from which we will be launching our docker container,as evident above we are using python 3.7 as the base layer, and in the next step we will be using docker compose, first before that lets set up our postgres database configuration
pip install psycopg2==2.9.5
pip install psycopg2-binary==2.9.5
pip freeze > requirements.txt
Go to the project "settings.py" file and add the following database configurations:
"settings.py"
from pathlib import Path
import os
from decouple import config
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-n18use^q@nwz&)s#os&zxlhy=3d57ys1wubfwi036e4h)7rol8'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ["*"]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'user_controller',
'Transanctions',
'Wallet'
]
MIDDLEWARE = [
'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 = 'E_WalletZ.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 = 'E_WalletZ.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DB_NAME = config("DB_NAME")
DB_USER = config("DB_USER")
DB_PASSWORD = config("DB_PASSWORD")
DB_HOST = config("DB_HOST")
DB_PORT = config("DB_PORT")
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': DB_NAME,
'USER': DB_USER,
'PASSWORD': DB_PASSWORD,
'HOST' : DB_HOST,
'PORT': DB_PORT,
}
}
# Password validation
# https://docs.djangoproject.com/en/4.2/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/4.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
#AWS CONFIG
# AWS CONFIG
AWS_STATIC_LOCATION = 'static'
S3_BUCKET_URL = config('S3_BUCKET_URL')
STATIC_ROOT = 'staticfiles'
AWS_ACCESS_KEY_ID = config('AWS_S3_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = config('AWS_S3_SECRET_ACCESS_KEY')
AWS_STORAGE_BUCKET_NAME = config('AWS_STORAGE_BUCKET_NAME')
AWS_HOST_REGION = config('AWS_HOST_REGION')
AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
AWS_DEFAULT_ACL = None
AWS_LOCATION = 'static'
MEDIA_URL = 'media/'
AWS_QUERYSTRING_AUTH = False
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),
]
STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/{AWS_LOCATION}/'
DEFAULT_FILE_STORAGE = 'E_WalletZ.storage_backends.MediaStorage'
STATICFILES_STORAGE = 'E_WalletZ.storage_backends.MediaStorage'
AWS_S3_OBJECT_PARAMETERS = {
'CacheControl': 'max-age=86400',
}
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
Now lets head back to our project root folder and add our docker-compose file "docker-compose.yml", create a file with the same name i.e
Inside that file add the following: (This file helps us define and manage multi-container Docker applications, in it as you will see we define the services, networks, volumes to be used and other configurations for our containers in a declarative format).
"docker-compose.yml"
version: "3"
services:
web:
build: .
command: bash -c "python manage.py runserver 0.0.0.0:8080"
container_name: E_WalletZ_API
restart: always
volumes:
- .:/E_WalletZ
ports:
- "8080:8080"
depends_on:
postgres:
condition: service_healthy
networks:
- E_WalletZ_net
postgres:
container_name: E_WalletZ_DB
image: postgres
restart: always
environment:
POSTGRES_USER: 'E_WalletZ_user'
POSTGRES_PASSWORD: 'E_WalletZ_password'
PGDATE: /data/E_WalletZ_postgres
healthcheck:
test: ["CMD-SHELL", "pg_isready -U E_WalletZ_user"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- postgres:/data/E_WalletZ_postgres
ports:
- '5432:5432'
networks:
- E_WalletZ_net
networks:
E_WalletZ_net:
driver: bridge
volumes:
postgres:
Wallet Users
Now before we spin up the containers for tests, lets code the user model, open "user_controller" folder
inside it lets open the "models.py" file and add the following code :
In the code we are using UserManager to control user creation in the project through the function create_user(), i.e in the case below we make sure user supplies email address, we also define how a superuser should be created. We then define our User class and its attributes, ImageUpload, UserProfile and UserAddress. (PS: please make use of chatGPT if you dont understand any of the code, the reason for this is if i go into deep explanation the article will grow longer than it already is)
"models.py"
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager
class UserManager(BaseUserManager):
def create_user(self, email, password, **extra_fields):
if not email:
raise ValueError("Email is required")
email = self.normalize_email(email)
user = self.model(email = email, **extra_fields)
user.set_password(password)
user.save()
return user
def create_superuser(self, email, password, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
extra_fields.setdefault('is_active',True)
extra_fields.setdefault('first_name', 'admin')
extra_fields.setdefault('last_name', 'admin')
if not extra_fields.get('is_staff', False):
raise ValueError('SuperUser must have is_staff=True')
if not extra_fiels.get('is_superuser', False):
raise ValueError('Superuser must have is_superuser=True')
return self.create_user(email, password, **extra_fields)
class User(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(unique=True)
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
is_staff = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
USERNAME_FIELD = "email"
objects = UserManager()
def __str__(self) -> str:
return self.email
def save(self, *args, **kwargs):
super().full_clean()
super().save(*args, **kwargs)
class ImageUpload(models.Model):
image = models.ImageField(upload_to="images")
def __str__(self):
return str(self.image)
class UserProfile(models.Model):
user = models.OneToOneField(User, related_name="user_profile", on_delete=models.CASCADE)
profile_picture = models.ForeignKey(ImageUpload, related_name="user_images", on_delete=models.SET_NULL, null=True)
dob = models.DateField()
phone = models.PositiveIntegerField()
country_code = models.CharField(default="+254", max_length=5)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.user.email
class UserAddress(models.Model):
user_profile = models.ForeignKey(UserProfile, related_name="user_addresses", on_delete=models.SET_NULL, null=True)
street = models.TextField()
city = models.CharField(max_length=100)
state = models.CharField(max_length=100)
country = models.CharField(max_length=100, default="Kenya")
is_default = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.user_profile.user.email
Now back to the project root folder(the one with Dockerfile and docker-compose.yml files). let run the following command :
docker-compose up
The output of this command will look something like:
It will build the image as defined in the Dockerfile and it will then create and launch the containers for the API and the Postgres Database using the compose file.
now lets open a new terminal and execute the following command to enter into the shell(bash) of the containers running our services.
first
docker ps
The command above will list the containers on your system, the result will be something like:
Now take the container ID to our API(E_WalletZ_API), from the command above and plug it into the command below:
docker exec -it <container_ID> /bin/bash
The result will look something like below, also notice on the last line i am executing the "collectstatic", the subsequent result of such command is shown below:
Now, after you have answered yes to the prompt, if you head to AWS Console and look into the bucket we just created, you will see the following:
Now, while still on this shell(bin/bash) for the API container, lets execute the following commands :
python manage.py makemigrations
python manage.py migrate
This is to test out our connection to the Postgres database, you should see something like the one below:
To confirm these migrations lets check into our database and check if the table has been created:
Exit out of your current E_WalletZ_API shell by executing the following command repeatedly till you end back to the terminal:
exit
from our terminal, we now want to enter into the shell for the postgres container, remember to do this we need the container ID for postgres container and we then plug it into the command below:
docker exec -it <container_id> /bin/bash
We will enter into the shell, while in the same shell execute the following command to link to the postgres database instance
psql -U E_WalletZ_user
Now if you are keen, u will remember in our .env file we had two variables with the value "E_WalletZ_user"
to list databases, the command is
\l
to connect to our database , the command is:
\c E_WalletZ_user
After, we connect to our, E_WalletZ_user database, we can list the tables as shown below:
\dt
JWT AUTHENTICATION
Lets work on an Authentication module for requests to resources
lets first install pyJWT as below :
PyJWT==2.0.0
pip freeze > requirements.txt
lets head into the folder that houses our "settings.py" and "urls.py" and here we will create a new file called "authentication.py"
"authentication.py"
from datetime import datetime
import jwt
from django.conf import settings
class TokenManager:
@staticmethod
def get_token(exp: int, payload, token_type="access"):
exp = datetime.now().timestamp() + (exp * 60)
return jwt.encode(
{"exp": exp, "type": token_type, **payload},
settings.SECRET_KEY,
algorithm = "HS256"
)
@staticmethod
def decode_token(token):
try:
decoded = jwt.decode(token, key=settings.SECRET_KEY, algorithms="HS256")
except jwt.DecodeError as e:
print("Cannot decode token because : ", e)
return None
if datetime.now().timestamp() > decoded["exp"]:
return None
return decoded
@staticmethod
def get_access(payload):
return TokenManager.get_token(100, payload)
@staticmethod
def get_refresh(payload):
return TokenManager.get_token(14*24*60, payload, "refresh")
class Authentication:
def __init__ (self, request):
self.request = request
def authenticate(self):
data = self.validate_request()
if not data:
return None
return self.get_user(data["user_id"])
def validate_request(self):
authorization = self.request.headers.get("Authorization", None)
if not authorization:
return None
token = authorization[4:]
decoded_data = TokenManager.decode_token(token)
if not decoded_data:
return None
return decoded_data
@staticmethod
def get_user(user_id):
from user_controller.models import User
try:
user = User.objects.get(id = user_id)
return user
except User.DoesNotExist:
return None
After the user authentication implemented above using JWT,below we implement pagination and querying formating below in a file called "permissions.py"
Create a permissions.py file in the same folder that holds our "urls.py", "wsgi.py" , "settings.py" i.e
Open the file and add the following code :
"permissions.py"
import graphene
from django.conf import settings
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
import re
from django.db.models import Q
def is_authenticated(func):
def wrapper(cls, info, **kwargs):
if not info.context.user:
raise Exception("You are not authorized to perform this operation")
return func(cls, info, **kwargs)
return wrapper
def paginate(model_type):
structure = {
"total" : graphene.Int(),
"size" : graphene.Int(),
"current_page": graphene.Int(),
"has_next" : graphene.Boolean(),
"has_prev" : graphene.Boolean(),
"results" : graphene.List(model_type)
}
return type(f"{model_type}Paginated", (graphene.ObjectType,), structure)
def resolve_paginated(query_data, info, page_info):
def get_paginated_data(qs, paginated_type, page):
page_size = settings.GRAPHENE.get("PAGE_SIZE", 10)
try:
qs.count()
except:
raise Exception(qs)
p = Paginator(qs, page_size)
try:
page_obj = p.page(page)
except PageNotAnInteger:
page_obj = p.page(1)
except EmptyPage:
page_obj = p.page(p.num_pages)
result = paginated_type.graphene_type(
total = p.num_pages,
size = qs.count(),
current_page = page_obj.number,
has_next = page_obj.has_next(),
has_prev = page_obj.has_previous(),
results = page_obj.object_list
)
return result
return get_paginated_data(query_data, info.return_type, page_info)
def normalize_query(query_string, findterms=re.compile(r'"([^"]+)"|(\S+)').findall, normspace=re.compile(r'\s{2,}').sub):
return [normspace(' ', (t[0] or t[1]).strip()) for t in findterms(query_string)]
def get_query(query_string, search_fields):
query = None #Query to search for every search term
terms = normalize_query(query_string)
for term in terms:
or_query = None #Query to search for a given term in each field
for field_name in search_fields:
q = Q(**{"%s__icontains" % field_name: term})
if or_query is None:
or_query = q
else:
or_query = or_query | q
if query is None:
query = or_query
else:
query = query & or_query
return query
GRAPHQL
Now lets plug in our graphql, schema..............
First things first, let install graphql packages to be used:
pip install graphene==2.1.8
pip install graphene-django==2.15.0
pip install graphene-file-upload==1.2.2
pip install graphql-core==2.3.2
pip install graphql-relay==2.0.1
pip freeze > requirements.txt
Head to the settings.py file, and add graphene_django as one of the installed apps, i.e :
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'graphene_django',
'user_controller',
'Transanctions',
'Wallet'
]
Let head into our user_controller app , in this folder we will create a new file "schema.py" to hold our graphql code as shown below:
Open the schema.py file and add the following code :
"schema.py"
import graphene
from .models import User, ImageUpload, UserProfile, UserAddress
from graphene_django import DjangoObjectType
from django.contrib.auth import authenticate
from datetime import datetime
from E_WalletZ.authentication import TokenManager
from E_WalletZ.permissions import paginate, is_authenticated
from django.utils import timezone
from graphene_file_upload.scalars import Upload
from django.conf import settings
class UserType(DjangoObjectType):
class Meta:
model = User
class ImageUploadType(DjangoObjectType):
image = graphene.String()
class Meta:
model = ImageUpload
def resolve_image(self, info):
if (self.image):
pass
class UserProfileType(DjangoObjectType):
class Meta:
model = UserProfile
class UserAddressType(DjangoObjectType):
class Meta:
model = UserAddress
class RegisterUser(graphene.Mutation):
status = graphene.Boolean()
message = graphene.String()
class Arguments:
email = graphene.String(required=True)
password = graphene.String(required=True)
first_name = graphene.String(required=True)
last_name = graphene.String(required=True)
def mutate(self, info, email, password, **kwargs):
User.objects.create_user(email, password, **kwargs)
return RegisterUser(
status = True,
message = "User has been Registered Succesfully"
)
class LoginUser(graphene.Mutation):
access = graphene.String()
refresh = graphene.String()
user = graphene.Field(UserType)
class Arguments:
email = graphene.String(required=True)
password = graphene.String(required=True)
def mutate(self, info, email, password):
user = authenticate(username=email, password=password)
if not user:
raise Exception("Invalid User credentials entered")
user.last_login = datetime.now(tz=timezone.utc)
user.save()
access = TokenManager.get_access({"user_id" : user.id })
refresh = TokenManager.get_refresh({"user_id" : user.id })
return LoginUser(
access = access,
refresh = refresh,
user = user
)
class GetAccess(graphene.Mutation):
access = graphene.String()
class Arguments:
refresh = graphene.String(required = True)
def mutate(self, info, refresh):
token = TokenManager.decode_token(refresh)
if not token or token["type"] != "refresh":
raise Exception("Invalid token or has expired, re-apply for fresh token")
access = TokenManager.get_access({ "user_id" : token["user_id"]})
return GetAccess(
access = access
)
class ImageUploadMain(graphene.Mutation):
image = graphene.Field(ImageUploadType)
class Arguments:
image = Upload(required=True)
def mutate(self, info, image):
image = ImageUpload.objects.create(image=image)
return ImageUploadMain(
image = image
)
class UserProfileInput(graphene.InputObjectType):
profile_picture = graphene.String()
country_code = graphene.String()
class CreateUserProfile(graphene.Mutation):
user_profile = graphene.Field(UserProfileType)
class Arguments:
profile_data = UserProfileInput()
dob = graphene.Date(required=True)
phone = graphene.Int(required=True)
@is_authenticated
def mutate(self, info, profile_data, **kwargs):
try:
info.context.user.user_profile
except Exception:
raise Exception("You don't hava a profile to update")
UserProfile.objects.filter(user_id = info.context.user.id).update(**profile_data, **kwargs)
return CreateUserProfile (
user_profile = info.context.user.user_profile
)
class UpdateUserProfile(graphene.Mutation):
user_profile = graphene.Field(UserProfileType)
class Arguments:
profile_data = UserProfileInput()
dob = graphene.Date()
phone = graphene.Int()
@is_authenticated
def mutate(self, info, profile_data, **kwargs):
try:
info.context.user.user_profile
except Exception:
raise Exception("You do not have a profile to update")
UserProfile.objects.filter(user_id = info.context.user.id).update(**profile_data, **kwargs)
return UpdateUserProfile(
user_profile = info.context.user.user_profile
)
class AddressInput(graphene.InputObjectType):
street = graphene.String()
city = graphene.String()
state = graphene.String()
country = graphene.String()
class CreateUserAddress(graphene.Mutation):
address = graphene.Field(UserAddressType)
class Arguments:
address_data = AddressInput(required=True)
is_default = graphene.Boolean()
@is_authenticated
def mutate(self, info, address_data, is_default=False):
try:
user_profile_id = info.context.user.user_profile.id
except Exception:
raise Exception("You need a profile to create an address")
existing_addresses = UserAddress.objects.filter(user_profile_id=user_profile_id)
if is_default:
existing_addresses.update(is_default=False)
address = UserAddress.objects.create(
user_profile_id = user_profile_id,
is_default = is_default,
**address_data
)
return CreateUserAddress(
address = address
)
class UpdateUserAddress(graphene.Mutation):
address = graphene.Field(UserAddressType)
class Arguments:
address_data = AddressInput()
is_default = graphene.Boolean()
address_id = graphene.ID(required=True)
@is_authenticated
def mutate(self, info, address_data, address_id, **kwargs):
profile_id = info.context.user.user_profile.id
address = UserAddress.objects.filter(
user_profile = profile_id,
id = address_id,
).update(is_default = is_default, **address_data)
if is_default:
UserAddress.objects.filter(user_profile_id=profile_id).exclude(id = address_id).update(is_default = False)
return UpdateUserAddress(
address = UserAddress.objects.get(id = address_id)
)
class DeleteUserAddress(graphene.Mutation):
status = graphene.Boolean()
class Arguments:
address_id = graphene.ID(required=True)
@is_authenticated
def mutate(self, info, address_id):
UserAddress.object.filter(
user_profile_id = profile_id,
id = address_id
).delete()
return DeleteUserAddress(
status = True
)
class Query(graphene.ObjectType):
users = graphene.Field(paginate(UserType), page=graphene.Int())
images = graphene.Field(paginate(ImageUploadType), page=graphene.Int())
me = graphene.Field(UserType)
def resolve_users(self, info, **kwargs):
print(User.objects.filter(**kwargs))
return User.objects.filter(**kwargs)
def resolve_images(self, info, **kwargs):
return ImageUpload.objects.filter(**kwargs)
def resolve_me(self, info):
return info.context.user
class Mutation(graphene.ObjectType):
register_user = RegisterUser.Field()
login_user = LoginUser.Field()
get_access = GetAccess.Field()
image_upload = ImageUploadMain.Field()
create_user_profile = CreateUserProfile.Field()
update_user_profile = UpdateUserProfile.Field()
create_user_address = CreateUserAddress.Field()
update_user_address = UpdateUserAddress.Field()
delete_user_address = DeleteUserAddress.Field()
schema = graphene.Schema(query=Query, mutation=Mutation)
Now, we need to unify the schemas from different apps into a project level schemas file :
In the folder that contains the "setting.py", "urls.py", "asgi.py" , add a file "schema.py" : i.e
Open the file "schema.py" and add the following code :
"schema.py"
import graphene
from user_controller.schema import schema as user_schema
class Query(user_schema.Query, graphene.ObjectType):
pass
class Mutation(user_schema.Mutation, graphene.ObjectType):
pass
schema = graphene.Schema(query=Query, mutation=Mutation)
We need to include setting for our graphene integration, head to the settings.py and add the GRAPHENE setting below:
"settings.py"
GRAPHENE = {
'SCHEMA': 'E_WalletZ.schema.schema',
'MIDDLEWARE': [
],
'PAGE_SIZE': 5
}
CORS_ALLOW_ALL_ORIGINS = True
still on this you will notice that our 'MIDDLEWARE' attributes is empty, this are middle-wares specific to graphql:
just next to the settings.py file, add a file named "middlewares.py"
open that file and add the following code:
from .permissions import resolve_paginated
class CustomAuthMiddleware(object):
#the resolve function can be used for resolving URL paths to the corresponding view functions, the signature is resolve(path, urlconf=None ), path is the URL path you want to resolve. The return type is a ResolverMatch object that allows one to access various metadata about th e ressolved URl.
def resolve(self, next, root, info, **kwargs):
info.context.user = self.authorize_user(info)
return next(root, info, **kwargs)
@staticmethod
def authorize_user(info):
from .authentication import Authentication
auth = Authentication(info.context)
return auth.authenticate()
class CustomPaginationMiddleware(object):
def resolve(self, next, root, info, **kwargs):
try:
is_paginated = info.return_type.name[-9:]
is_paginated = is_paginated == "Paginated"
except Exception:
is_paginated = False
if is_paginated:
page = kwargs.pop("page", 1)
return resolve_paginated(next(root, info, **kwargs).value, info, page)
return next(root, info, **kwargs)
Now back to the "settings.py" open it up and add the two custom Middlewares we just created:
GRAPHENE = {
'SCHEMA': 'E_WalletZ.schema.schema',
'MIDDLEWARE': [
'E_WalletZ.middlewares.CustomAuthMiddleware',
'E_WalletZ.middlewares.CustomPaginationMiddleware'
],
'PAGE_SIZE': 5
}
CORS_ALLOW_ALL_ORIGINS = True
With the graphene integration completed, we now need to push our graphene_django static files to AWS S3, remember what we did in the initial step
List the containers using the command below :
docker ps
Pick the container id of the container that is running the API
Use that "container id" in the command below to enter into the shell
docker exec -it <container_id> /bin/bash
Now after we have succesfully enter into the bash of the container, we execute the commands below to push the files to AWS
python manage.py collectstatic
After this, if you check your AWS bucket it now has another new file called "graphene_django" :
Now lets head to our "urls.py" file and add the following code :
"urls.py"
from django.contrib import admin
from django.urls import path
from django.conf import settings
from django.conf.urls.static import static
from graphene_django.views import GraphQLView
from django.views.decorators.csrf import csrf_exempt
from graphene_file_upload.django import FileUploadGraphQLView
urlpatterns = [
path('admin/', admin.site.urls),
path('graphview/', csrf_exempt(FileUploadGraphQLView.as_view(graphiql=True)))
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
On your container logs you should now see the following:
if u copy and paste the url on your browser(in my case it is http://0.0.0.0:8000) .you should now see the graphql UI as shown below:
WALLETS MODEL AND SCHEMA
The Wallets app will be the object that holds the token or cash or representation of such that will be attached to a user object that we have already created above.
Open up the "models.py" file, here we want to create a model object to represent a wallet:
"models.py"
from django.db import models
from django.contrib.auth import get_user_model
from djmoney.models.fields import MoneyField
from decimal import Decimal
from djmoney.models.validators import MaxMoneyValidator, MinMoneyValidator
#from Transanctions.models import Transanction
User = get_user_model()
# Create your models here.
class Wallet(models.Model):
user = models.OneToOneField(User, related_name="user_wallet", on_delete=models.PROTECT)
""" amount_available = MoneyField(
max_digits=11, decimal_places=2, default=0, default_currency='KES',
validators=[
MinMoneyValidator(Decimal(0.00)), MaxMoneyValidator(Decimal(999999999.99)),
]
)
"""
amount_available = models.DecimalField(max_digits=11, decimal_places=2, default=0.00)
created_on = models.DateTimeField(auto_now_add=True)
updated_on = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.user.firstName}-{self.user.lastName}-wallet"
class WalletProfile(models.Model):
TYPES = {
(0, "Mini"),
(1, "Regular"),
(2, "Super"),
}
LIMITS = {
(0, 100000),
(1, 1000000),
(2, 100000000),
(3, 9999999999.99)
}
name = models.PositiveSmallIntegerField(default=1, choices=TYPES)
limit = models.PositiveSmallIntegerField(default=1, choices=LIMITS)
wallet = models.OneToOneField(Wallet, related_name="wallet_profile", on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.name}-WithLimit-{self.limit}"
Next we want to pipe the model and its attributes through grapqhl, sort of build a wrapper around it.
create a file called schema.py and add the following code:
"schema.py"
import graphene
from .models import Wallet, WalletProfile
from graphene_django import DjangoObjectType
from django.contrib.auth import authenticate
from datetime import datetime
from E_WalletZ.authentication import TokenManager
from E_WalletZ.permissions import paginate, is_authenticated
#from django.utils import timezone
#from django.conf import settings
from decimal import Decimal
from graphql.language import ast
from django.contrib.auth import get_user_model
User = get_user_model()
class WalletType(DjangoObjectType):
class Meta:
model = Wallet
class WalletProfileType(DjangoObjectType):
class Meta:
model = WalletProfile
class RegisterWallet(graphene.Mutation):
status = graphene.Boolean()
message = graphene.String()
@is_authenticated
def mutate(self, info, **kwargs):
user = User.objects.get(id=info.context.user.id)
Wallet.objects.create(user=user)
return RegisterWallet(
status= True,
message = "Wallet Created Succesfully"
)
class CreditWallet(graphene.Mutation):
status = graphene.Boolean()
message = graphene.String()
class Arguments:
credit_amount = graphene.Float()
@is_authenticated
def mutate(self, info, credit_amount, **kwargs):
#credit_amount = graphene.Decimal(credit_amount)
wallet = Wallet.objects.get(user_id=info.context.user.id)
wallet.amount_available += Decimal(str(credit_amount))
wallet.save()
return CreditWallet(
status = True,
message = f"Your Wallet has been successfully credit with the amount {credit_amount}"
)
class DeleteUserWallet(graphene.Mutation):
status = graphene.Boolean()
@is_authenticated
def mutate(self, info, **kwargs):
Wallet.objects.filter(user_id=info.context.user.id).delete()
return DeleteUserWallet(
status = True
)
class Query(graphene.ObjectType):
wallet = graphene.Field(paginate(WalletType), page = graphene.Int())
@is_authenticated
def resolve_wallet(self, info):
return Wallet.objects.filter(user_id=info.context.user.id)
class Mutation(graphene.ObjectType):
register_wallet = RegisterWallet.Field()
credit_wallet = CreditWallet.Field()
delete_user_wallet = DeleteUserWallet.Field()
schema = graphene.Schema(query=Query, mutation=Mutation)
Now that we have the wallet schema built, we have to head to the project level schema file and plug it there:
So head to the folder that houses "urls.py", "settings.py" i.e :
open the schema.py file and add the following code:
"schema.py"
import graphene
from user_controller.schema import schema as user_schema
from Wallet.schema import schema as wallet_schema
class Query(user_schema.Query, wallet_schema.Query, graphene.ObjectType):
pass
class Mutation(user_schema.Mutation, wallet_schema.Mutation, graphene.ObjectType):
pass
schema = graphene.Schema(query=Query, mutation=Mutation)
Now with all changes let migrate the model wallets to the postgres database:
remember what we did before:
list containers using the command :
docker ps
Note the container id for the container that is running the API, i.e E_WalletZ_API and plug it into the following command:
docker exec -it <container_id> /bin/bash
Now in the shell lets make the migrations; to do this we use the commands below:
python manage.py makemigrations
python manage.py migrate
The result should look something like:
Now lets head to the postgres database shell to confirm the wallets table has been created:
do the same thing we did above, now in this case enter the postgres' container id in the command:
docker exec -it <container_id> /bin/bash
Once in the container shell, lets use the command below to log into our database:
psql -U E_WalletZ_user
Then execute the commands shown below to see if wallets table has been created:
Now to our graphiql API UI, let see if the changes have reflected; you should ideally see the following:
wallah ! , we are done with Wallets app ,now lets head to Transanctions app
Transanctions Model and Schema
Open the folder to the Transanctions app, and add the following code to the models.py file
"models.py"
from django.db import models
from django.contrib.auth import get_user_model
from user_controller.models import User
from djmoney.models.validators import MaxMoneyValidator, MinMoneyValidator
from decimal import Decimal
from djmoney.models.fields import MoneyField
from Wallet.models import Wallet
transactee = get_user_model()
class Transanction(models.Model):
wallet = models.ForeignKey(Wallet, related_name="transanction_wallet", on_delete=models.PROTECT)
sender = models.OneToOneField(transactee, related_name="sender_transanction", on_delete=models.PROTECT)
receiver = models.OneToOneField(transactee, related_name="receiver_transanction", on_delete=models.PROTECT)
"""amount = MoneyField(
max_digits=7, decimal_places=2, default=0, default_currency='KES',
validators=[
MinMoneyValidator(Decimal(0.00)), MaxMoneyValidator(Decimal(99999.99))
]
)
"""
amount = models.DecimalField(max_digits=7, decimal_places=2, default=0.00)
created_on = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.sender.firstName}_{self.sender.lastName}-to-{self.receiver.firstName}_{self.receiver.lastName}={self.amount}-{self.created_on}"
class TransanctionProfile(models.Model):
TYPE = {
(0, "AFRICA"),
(1, "KENYA"),
(2, "WORLD")
}
CATEGORY = {
(0, "0 - 100,000"),
(1, "100,000 - 1,000,000"),
(2, "1,000,000 - 100,000,000"),
(3, "100,000,000 - 500,000,000"),
(4, "> 500,000,000")
}
transanction = models.OneToOneField(Transanction, related_name="transanction_profile", on_delete=models.CASCADE)
transanction_category = models.PositiveSmallIntegerField(default=0, choices=CATEGORY)
transanction_type = models.PositiveSmallIntegerField(default=1, choices=TYPE)
def __str__(self):
return f"{self.transanction_category}-{self.transanction_type}"
Now lets wrap around this model using , a graphql schema; create a file "schema.py" in it add the following code:
before that lets create a movemoney.py module to execute the logic of moving money from one wallet to another. You will notice that the movemoney module is imported by the "schema.py"
"movemoney.py"
class MovingMoney:
@staticmethod
def movemoney(info,sender,receiver,wallet,amount):
from Wallet.models import Wallet
import decimal
print(f"Cash transfer of {amount} from you to {receiver.first_name}-{receiver.last_name} starting")
try:
sender_wallet = Wallet.objects.get(user_id=sender.id)
sender_wallet.amount_available = (sender_wallet.amount_available - decimal.Decimal(str(amount)).quantize(decimal.Decimal('.01')))
sender_wallet.save()
except Exception:
return Exception("Updating of the senders amount_available failed")
try:
receiver_wallet = Wallet.objects.get(user_id = receiver.id)
receiver_wallet.amount_available = (receiver_wallet.amount_available + decimal.Decimal(str(amount)).quantize(decimal.Decimal('.01')))
receiver_wallet.save()
except Exception:
sender_wallet.amount_available = (sender_wallet.amount_available + decimal.Decimal(str(amount)).quantize(decimal.Decimal('.01')))
return Exception("Money transfer to receiver has failed")
print ("Money Transfer is now complete!")
"schema.py"
import graphene
from .models import Transanction, TransanctionProfile
from graphene_django import DjangoObjectType
from django.contrib.auth import authenticate
from datetime import datetime
from E_WalletZ.authentication import TokenManager
from E_WalletZ.permissions import paginate, is_authenticated
from user_controller.models import User
from Wallet.models import Wallet
from .movemoney import MovingMoney
CONST_TRANSANCTION_FEE_PERCENTAGE = 0.25
class TransanctionType(DjangoObjectType):
class Meta:
model = Transanction
class TransanctionProfile(DjangoObjectType):
class Meta:
model = TransanctionProfile
class MakeTransanction(graphene.Mutation):
status = graphene.Boolean()
message = graphene.String()
class Arguments:
amount_to_send = graphene.Float()
email = graphene.String()
@is_authenticated
def mutate(self, info, email, amount_to_send):
receiver = User.objects.get(email=email)
if (receiver.id == info.context.user.id):
return Exception("Cant send Money to yourself")
user = User.objects.get(id = info.context.user.id)
receiver_wallet = Wallet.objects.get(user_id=receiver.id)
sender_wallet = Wallet.objects.get(user_id=info.context.user.id)
if sender_wallet.amount_available <= ((amount_to_send*CONST_TRANSANCTION_FEE_PERCENTAGE)+amount_to_send):
return Exception(f"You have insufficient funds '{Wallet_user.amount_available}' to complete the cash transfer and pay charges, top up to continue")
#amount_to_send = graphene.Decimal(amount_to_send)
try:
Transanction.objects.create(wallet=sender_wallet, sender=user, receiver=receiver, amount=amount_to_send)
Transanction.objects.create(wallet=receiver_wallet, sender=user, receiver=receiver, amount=amount_to_send)
#MovingMoney.movemoney(info=info, sender=user, receiver=receiver,wallet=Wallet ,amount=amount_to_send)
except Exception:
return Exception("Transanction failed please try again")
MovingMoney.movemoney(info=info, sender=user, receiver=receiver,wallet=Wallet,amount=amount_to_send)
return MakeTransanction(
status= True,
message = f"You have succesfully transfered {amount_to_send} from your wallet to {receiver.email}"
)
class DeleteTransanction(graphene.Mutation):
status = graphene.Boolean()
message = graphene.String()
class Arguments:
transanction_id = graphene.ID(required=True)
@is_authenticated
def mutate(self, info, transanction_id):
Wallet = Wallet.objects.get(user_id=info.context.user.id)
Transanction.object.filter(
wallet=Wallet,
id = transanction_id
).delete()
return DeleteTransanction(
status = True,
message = f"you have succesfully deleted the transanction {transanction_id}"
)
class Query(graphene.ObjectType):
transanctions = graphene.Field(paginate(TransanctionType), page=graphene.Int())
@is_authenticated
def resolve_transanctions(self,info):
wallet = Wallet.objects.get(user_id=info.context.user.id)
print("Wallet :", wallet)
return Transanction.objects.filter(wallet_id=wallet.id)
class Mutation(graphene.ObjectType):
make_transanction = MakeTransanction.Field()
delete_transanction = DeleteTransanction.Field()
schema = graphene.Schema(query=Query, mutation=Mutation)
Similar to how we did with the wallets app, lets do the same with the Transanctions app, we first head to the project level schema.py file to plug the transanctions model schema i.e
Go to the folder that holds the "settings.py" and "urls.py"
"schema.py"
import graphene
from user_controller.schema import schema as user_schema
from Transanctions.schema import schema as transanction_schema
from Wallet.schema import schema as wallet_schema
class Query(user_schema.Query, transanction_schema.Query, wallet_schema.Query, graphene.ObjectType):
pass
class Mutation(user_schema.Mutation, transanction_schema.Mutation, wallet_schema.Mutation, graphene.ObjectType):
pass
schema = graphene.Schema(query=Query, mutation=Mutation)
Next, we make migrations to update our database tables, with the transanctions model:
Follow the process i mentioned above to enter into the container shell, your results should look like :
confirming in the postgres database :
When we check on graphiQL:
Time for a well earned break, on the next article we work on the testing the API by creating users and wallets, we then simulate the deposit of cash, the transfer of cash between wallet all from Graphiql and also build a jenkins CI/CD pipeline for automated deployment on AWS EC2 instances.Goodbye , see you then.
If you get the error: ImportError: cannot import name 'Mapping' from 'collections' (/usr/local/lib/python3.10/collections/init.py) : find the solution here
Top comments (0)