DEV Community

Cover image for Lets Build a Digital /E-Wallets API
myk_okoth_ogodo
myk_okoth_ogodo

Posted on

Lets Build a Digital /E-Wallets API

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
Enter fullscreen mode Exit fullscreen mode

"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$ 
Enter fullscreen mode Exit fullscreen mode

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$ 
Enter fullscreen mode Exit fullscreen mode

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 

Enter fullscreen mode Exit fullscreen mode

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 fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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:

Image description

choose the region as "Europe(London) eu-west-2", on the Object Ownership choose "ACLs enabled " as shown below :

Image description

Image description

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:

Image description

One last step ......

On the "e-walletz" page, click on "permissions" tab and scroll down to bucket policy, click on edit :

Image description

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)

Image description

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:

Image description

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/'

Enter fullscreen mode Exit fullscreen mode

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."
Enter fullscreen mode Exit fullscreen mode

Now head to the "settings.py" file, located here :

Image description

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
Enter fullscreen mode Exit fullscreen mode

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'


Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

head back to the folder that held the "settings.py" file and create a new file "storage_backends.py"

Image description

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

Enter fullscreen mode Exit fullscreen mode

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'

Enter fullscreen mode Exit fullscreen mode

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":

Image description

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


Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'

Enter fullscreen mode Exit fullscreen mode

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

Image description

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:

Enter fullscreen mode Exit fullscreen mode

Wallet Users

Now before we spin up the containers for tests, lets code the user model, open "user_controller" folder

Image description

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


Enter fullscreen mode Exit fullscreen mode

Now back to the project root folder(the one with Dockerfile and docker-compose.yml files). let run the following command :

docker-compose up
Enter fullscreen mode Exit fullscreen mode

The output of this command will look something like:

Image description

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
Enter fullscreen mode Exit fullscreen mode

The command above will list the containers on your system, the result will be something like:

Image description

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
Enter fullscreen mode Exit fullscreen mode

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:

Image description

The result of collectstatic:
Image description

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:

Image description

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
Enter fullscreen mode Exit fullscreen mode

This is to test out our connection to the Postgres database, you should see something like the one below:

Image description

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now if you are keen, u will remember in our .env file we had two variables with the value "E_WalletZ_user"

Image description

to list databases, the command is

\l
Enter fullscreen mode Exit fullscreen mode

to connect to our database , the command is:

\c E_WalletZ_user
Enter fullscreen mode Exit fullscreen mode

After, we connect to our, E_WalletZ_user database, we can list the tables as shown below:

\dt
Enter fullscreen mode Exit fullscreen mode

Image description

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
Enter fullscreen mode Exit fullscreen mode

lets head into the folder that houses our "settings.py" and "urls.py" and here we will create a new file called "authentication.py"

Image description

"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
Enter fullscreen mode Exit fullscreen mode

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

Image description

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
]
Enter fullscreen mode Exit fullscreen mode

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:
Image description

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)

Enter fullscreen mode Exit fullscreen mode

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

Image description

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)

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

After this, if you check your AWS bucket it now has another new file called "graphene_django" :
Image description

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)
Enter fullscreen mode Exit fullscreen mode

On your container logs you should now see the following:

Image description

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:

Image description

Image description

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.

Image description

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}"
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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 :

Image description

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)

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now in the shell lets make the migrations; to do this we use the commands below:

python manage.py makemigrations
python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

The result should look something like:
Image description

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

Enter fullscreen mode Exit fullscreen mode

Once in the container shell, lets use the command below to log into our database:

psql -U E_WalletZ_user
Enter fullscreen mode Exit fullscreen mode

Then execute the commands shown below to see if wallets table has been created:

Image description

Now to our graphiql API UI, let see if the changes have reflected; you should ideally see the following:

Image description

Image description

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

Image description

"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}"
Enter fullscreen mode Exit fullscreen mode

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!")

Enter fullscreen mode Exit fullscreen mode

"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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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 :

Image description

confirming in the postgres database :

Image description

When we check on graphiQL:

Image description

Image description

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)