feat: backend with models

Signed-off-by: Ameya Shenoy <shenoy.ameya@gmail.com>
This commit is contained in:
Ameya Shenoy 2023-03-19 19:12:47 +05:30
parent 359b935fc4
commit 6bb49337c3
Signed by: codingcoffee
GPG key ID: EEC8EA855D61CEEC
20 changed files with 1028 additions and 7 deletions

174
backend/.gitignore vendored Normal file
View file

@ -0,0 +1,174 @@
# Created by https://www.toptal.com/developers/gitignore/api/python
# Edit at https://www.toptal.com/developers/gitignore?templates=python
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# End of https://www.toptal.com/developers/gitignore/api/python
# App Specific
media/
media_cdn/
static/
static_cdn/

15
backend/Dockerfile.dev Normal file
View file

@ -0,0 +1,15 @@
FROM python:3.10.9-slim-bullseye
LABEL maintainer "Ameya Shenoy <shenoy.ameya@gmail.com>"
ENV PYTHONUNBUFFERED=1
ENV CRYPTOGRAPHY_DONT_BUILD_RUST=1
WORKDIR /code
COPY requirements.txt /
RUN set -ex \
&& pip install -r /requirements.txt \
&& rm -rf requirements.txt

View file

16
backend/backend/asgi.py Normal file
View file

@ -0,0 +1,16 @@
"""
ASGI config for backend project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
application = get_asgi_application()

249
backend/backend/settings.py Normal file
View file

@ -0,0 +1,249 @@
"""
Django settings for backend project.
Generated by 'django-admin startproject' using Django 4.1.7.
For more information on this file, see
https://docs.djangoproject.com/en/4.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.1/ref/settings/
"""
import os
from pathlib import Path
# 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.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get("SECRET_KEY", "sample-for-collectstatic")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ.get("DEBUG", "0").lower() in ["1", "true", "True", "yes"]
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "").split(",")
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"whitenoise.runserver_nostatic",
"django.contrib.staticfiles",
"drf_yasg",
"django_rq",
"rest_framework",
"django_extensions",
"foldbank",
"corsheaders",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.middleware.common.CommonMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "backend.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 = "backend.wsgi.application"
# Database
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("POSTGRES_DB"),
"USER": os.environ.get("POSTGRES_USER"),
"PASSWORD": os.environ.get("POSTGRES_PASSWORD"),
"HOST": os.environ.get("POSTGRES_SERVER"),
"PORT": os.environ.get("POSTGRES_PORT"),
"CONN_MAX_AGE": int(os.environ.get("POSTGRES_CONN_MAX_AGE", 60)),
"ATOMIC_REQUESTS": True,
},
}
# Password validation
# https://docs.djangoproject.com/en/4.1/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.1/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.1/howto/static-files/
STATIC_URL = "static/"
MEDIA_URL = "media/"
STATIC_ROOT = os.path.join(BASE_DIR, "static_cdn")
MEDIA_ROOT = os.path.join(BASE_DIR, "media_cdn")
STATICFILES_DIRS = [
BASE_DIR / "static",
BASE_DIR / "media",
]
# Default primary key field type
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
DJANGO_LOG_LEVEL = os.environ.get("DJANGO_LOG_LEVEL", "INFO")
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"level": DJANGO_LOG_LEVEL,
"class": "logging.StreamHandler",
"formatter": "console",
},
"rq_console": {
"level": DJANGO_LOG_LEVEL,
"class": "rq.utils.ColorizingStreamHandler",
"formatter": "rq_console",
"exclude": ["%(asctime)s"],
},
},
"formatters": {
"rq_console": {
"format": "%(asctime)s | %(name)s | %(levelname)s | %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
},
"console": {
"format": "%(asctime)s | %(name)s | %(levelname)s | %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
},
},
"loggers": {
"rq.worker": {
"handlers": ["rq_console"],
"level": DJANGO_LOG_LEVEL,
},
},
"root": {
"handlers": ["console"],
"level": DJANGO_LOG_LEVEL,
},
}
RQ_SHOW_ADMIN_LINK = True
REDIS_HOST = os.environ.get("REDIS_HOST", "redis")
REDIS_PORT = os.environ.get("REDIS_PORT", 6379)
REDIS_QUEUE_DB = os.environ.get("REDIS_QUEUE_DB", 0)
REDIS_DEFAULT_TIMEOUT = os.environ.get("REDIS_RQ_DEFAULT_TIMEOUT", 360)
REDIS_CACHE_DB = os.environ.get("REDIS_CACHE_DB", 1)
RQ_QUEUES = {
"default": {
"HOST": REDIS_HOST,
"PORT": REDIS_PORT,
"DB": REDIS_QUEUE_DB,
"DEFAULT_TIMEOUT": REDIS_DEFAULT_TIMEOUT,
},
}
AUTH_USER_MODEL = "foldbank.User"
CSRF_TRUSTED_ORIGINS = os.environ.get("CSRF_TRUSTED_ORIGINS", "").split(",")
CORS_ALLOWED_ORIGINS = os.environ.get(
"CORS_ALLOWED_ORIGINS", "https://foldbank.coingcoffee.me"
).split(",")
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
DEFAULT_RENDERER_CLASSES = (
"rest_framework.renderers.JSONRenderer",
"rest_framework.renderers.BrowsableAPIRenderer",
)
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
"DEFAULT_PARSER_CLASSES": ("rest_framework.parsers.JSONParser",),
"SEARCH_PARAM": "q",
"DEFAULT_THROTTLE_CLASSES": [
"rest_framework.throttling.AnonRateThrottle",
"rest_framework.throttling.UserRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {"anon": "100/day", "user": "1000/day"},
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
"PAGE_SIZE": 5,
"DEFAULT_RENDERER_CLASSES": DEFAULT_RENDERER_CLASSES,
}
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": f"redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_CACHE_DB}",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
}
}
SWAGGER_SETTINGS = {
"SECURITY_DEFINITIONS": {
"Bearer": {"type": "apiKey", "name": "Authorization", "in": "header"}
}
}

29
backend/backend/urls.py Normal file
View file

@ -0,0 +1,29 @@
"""backend URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path
urlpatterns = [
path('admin/', admin.site.urls),
]
if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

16
backend/backend/wsgi.py Normal file
View file

@ -0,0 +1,16 @@
"""
WSGI config for backend project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
application = get_wsgi_application()

View file

96
backend/foldbank/admin.py Normal file
View file

@ -0,0 +1,96 @@
from django.contrib import admin
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django import forms
from django.contrib.auth.admin import UserAdmin
from foldbank.models import (
Account,
Bank,
RecurringPayment,
RecurringPaymentTransactionLink,
Tag,
Transaction,
User,
)
class UserForm(forms.ModelForm):
password = ReadOnlyPasswordHashField(
help_text=(
"Raw passwords are not stored, "
"so there is no way to see this users password, "
"but you can change the password using "
"<a href='../password/'>this form</a>."
)
)
class Meta:
fields = "__all__"
model = User
class UserAdminConfig(UserAdmin):
form = UserForm
search_fields = ("username", "id",)
readonly_fields = (
"id",
"date_joined",
"last_login",
"last_updated",
)
list_filter = ("is_staff", "is_superuser", "is_active")
ordering = ("-date_joined",)
list_display = ("username", "name", "last_login", "is_active", "is_superuser")
fieldsets = (
(
"Identifiers",
{"fields": ("id", "name", "username", "password",)},
),
(
"Personal",
{
"fields": (
"last_login",
"last_updated",
"date_joined",
)
},
),
(
"Permissions",
{
"fields": (
"is_active",
"is_staff",
"is_superuser",
"groups",
"user_permissions",
)
},
),
)
add_fieldsets = (
(
None,
{
"classes": ("wide",),
"fields": (
"username",
"password1",
"password2",
),
},
),
)
# Register your models here.
admin.site.register(User, UserAdminConfig)
admin.site.register(Tag)
admin.site.register(Bank)
admin.site.register(Account)
admin.site.register(RecurringPayment)
admin.site.register(Transaction)
admin.site.register(RecurringPaymentTransactionLink)

6
backend/foldbank/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class FoldbankConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'foldbank'

View file

@ -0,0 +1,125 @@
# Generated by Django 4.1.7 on 2023-03-19 13:39
import datetime
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import foldbank.utils
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('password', models.CharField(max_length=128, verbose_name='password')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('username', models.CharField(blank=True, help_text='only allows letters, numbers and underscores', max_length=32, null=True, unique=True, validators=[django.core.validators.RegexValidator('^[a-zA-Z0-9_]*$', message='only allows letters, numbers and underscores')])),
('name', models.CharField(blank=True, max_length=128, null=True)),
('is_active', models.BooleanField(default=True)),
('is_staff', models.BooleanField(default=False)),
('is_superuser', models.BooleanField(default=False)),
('date_joined', models.DateTimeField(auto_now_add=True)),
('last_login', models.DateTimeField(default=django.utils.timezone.now)),
('last_updated', models.DateTimeField(auto_now=True)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Account',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('account_number', models.CharField(blank=True, max_length=64, null=True)),
('account_type', models.CharField(choices=[('SA', 'Savings Account'), ('CA', 'Current Account')], default='SA', max_length=2)),
('ifsc_code', models.CharField(blank=True, max_length=64, null=True)),
('swift_bic', models.CharField(blank=True, max_length=64, null=True)),
('holders_name', models.CharField(blank=True, max_length=64, null=True)),
('account_owner_type', models.CharField(choices=[('UA', 'User Account'), ('OA', 'Organization Account')], default='UA', max_length=2)),
('balance', models.FloatField(blank=True, null=True)),
],
),
migrations.CreateModel(
name='Bank',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(blank=True, max_length=64, null=True)),
('logo', models.FileField(blank=True, default=None, null=True, upload_to=foldbank.utils.get_bank_logo_file_path)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name='RecurringPayment',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('frequency', models.DurationField(default=datetime.timedelta(days=30))),
('amount', models.FloatField(blank=True, null=True)),
('due_on', models.DateTimeField(auto_now_add=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('from_account', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='recurring_payment_from', to='foldbank.account')),
],
),
migrations.CreateModel(
name='Tag',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('title', models.CharField(blank=True, max_length=64, null=True)),
('sub_category', models.CharField(blank=True, max_length=64, null=True)),
('icon_type', models.CharField(blank=True, max_length=64, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name='Transaction',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('amount', models.FloatField(blank=True, null=True)),
('from_account', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='transactions_from', to='foldbank.account')),
('tag', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='foldbank.tag')),
('to_account', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='transactions_to', to='foldbank.account')),
],
),
migrations.CreateModel(
name='RecurringPaymentTransactionLink',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('recurring_payment', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='foldbank.recurringpayment')),
('transaction', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='foldbank.transaction')),
],
),
migrations.AddField(
model_name='recurringpayment',
name='tag',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='foldbank.tag'),
),
migrations.AddField(
model_name='recurringpayment',
name='to_account',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='recurring_payment_to', to='foldbank.account'),
),
migrations.AddField(
model_name='account',
name='bank',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='foldbank.bank'),
),
migrations.AddField(
model_name='account',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
),
]

View file

175
backend/foldbank/models.py Normal file
View file

@ -0,0 +1,175 @@
import uuid
from datetime import datetime, timedelta
from django.contrib.auth.models import (
AbstractBaseUser,
BaseUserManager,
PermissionsMixin,
)
from django.core.validators import RegexValidator
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext as _
from foldbank.utils import get_bank_logo_file_path
# object managers
class CustomAccountManager(BaseUserManager):
def create_user(self, username, password):
if not username:
raise ValueError("Users must have a username.")
user = self.model(username=username)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, username, password):
user = self.create_user(username, password)
user.is_staff = True
user.is_superuser = True
user.save(using=self._db)
return user
# Create your models here.
class User(AbstractBaseUser, PermissionsMixin):
# TODO: make username case insensitive yet retain case sensitivity
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
username = models.CharField(
max_length=32,
unique=True,
null=True,
blank=True,
validators=[
RegexValidator(
r"^[a-zA-Z0-9_]*$",
message="only allows letters, numbers and underscores",
)
],
help_text=_("only allows letters, numbers and underscores"),
)
name = models.CharField(null=True, blank=True, max_length=128)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
is_superuser = models.BooleanField(default=False)
date_joined = models.DateTimeField(auto_now_add=True)
last_login = models.DateTimeField(default=timezone.now)
last_updated = models.DateTimeField(auto_now=True)
objects = CustomAccountManager()
USERNAME_FIELD = "username"
def __str__(self):
if not self.username:
return "USERNAME-NOT-SET"
return self.username
class Tag(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
title = models.CharField(blank=True, null=True, max_length=64)
sub_category = models.CharField(blank=True, null=True, max_length=64)
icon_type = models.CharField(blank=True, null=True, max_length=64)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.title} - {self.sub_category}"
class Bank(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(blank=True, null=True, max_length=64)
logo = models.FileField(
upload_to=get_bank_logo_file_path,
null=True,
blank=True,
default=None,
)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
class Account(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
created_at = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey("User", on_delete=models.PROTECT)
bank = models.ForeignKey("Bank", on_delete=models.PROTECT)
account_number = models.CharField(blank=True, null=True, max_length=64)
SAVINGS_ACCOUNT = "SA"
CURRENT_ACCOUNT = "CA"
ACCOUNT_TYPES = [
(SAVINGS_ACCOUNT, "Savings Account"),
(CURRENT_ACCOUNT, "Current Account"),
]
account_type = models.CharField(
max_length=2,
choices=ACCOUNT_TYPES,
default=SAVINGS_ACCOUNT,
)
ifsc_code = models.CharField(blank=True, null=True, max_length=64)
swift_bic = models.CharField(blank=True, null=True, max_length=64)
holders_name = models.CharField(blank=True, null=True, max_length=64)
USER_ACCOUNT = "UA"
ORG_ACCOUNT = "OA"
ACCOUNT_OWNER_TYPES = [
(USER_ACCOUNT, "User Account"),
(ORG_ACCOUNT, "Organization Account"),
]
account_owner_type = models.CharField(
max_length=2,
choices=ACCOUNT_OWNER_TYPES,
default=USER_ACCOUNT,
)
balance = models.FloatField(null=True, blank=True)
def __str__(self):
return f"{self.account_owner_type} - {self.bank} - {self.account_number} - {self.holders_name}"
class Transaction(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
created_at = models.DateTimeField(auto_now_add=True)
amount = models.FloatField(null=True, blank=True)
tag = models.ForeignKey("Tag", on_delete=models.PROTECT)
from_account = models.ForeignKey(
"Account", on_delete=models.PROTECT, related_name="transactions_from"
)
to_account = models.ForeignKey(
"Account", on_delete=models.PROTECT, related_name="transactions_to"
)
def __str__(self):
return f"{self.from_account.holders_name} - {self.to_account.holders_name}"
class RecurringPayment(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
from_account = models.ForeignKey(
"Account", on_delete=models.PROTECT, related_name="recurring_payment_from"
)
to_account = models.ForeignKey(
"Account", on_delete=models.PROTECT, related_name="recurring_payment_to"
)
frequency = models.DurationField(default=timedelta(days=30))
amount = models.FloatField(null=True, blank=True)
due_on = models.DateTimeField(auto_now_add=True)
created_at = models.DateTimeField(auto_now_add=True)
tag = models.ForeignKey("Tag", on_delete=models.PROTECT)
def __str__(self):
return f"{self.from_account.holders_name} - {self.to_account.holders_name}"
class RecurringPaymentTransactionLink(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
recurring_payment = models.ForeignKey("RecurringPayment", on_delete=models.PROTECT)
transaction = models.ForeignKey("Transaction", on_delete=models.PROTECT)

View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

19
backend/foldbank/utils.py Normal file
View file

@ -0,0 +1,19 @@
import pathlib
import datetime
def get_file_extension(file):
return pathlib.Path(file.name).suffix
def generate_filename_for_storage(file):
ext = get_file_extension(file)
filename = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
return f"{filename}{ext}"
def get_bank_logo_file_path(self, _):
filename = generate_filename_for_storage(self.logo)
return f"images/bank/{self.pk}/logo/{filename}"

View file

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

22
backend/manage.py Executable file
View file

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

16
backend/requirements.txt Normal file
View file

@ -0,0 +1,16 @@
django==4.1.7
psycopg2-binary==2.9.3 # to support PostgreSQL as a DB backend
djangorestframework==3.14.0 # DJango REST Framework
markdown==3.4.1 # Markdown support for the browsable API for DJango REST Framework
django-filter==22.1 # Filtering support for DJango REST Framework
django-rq==2.7.0 # for django rq for background jobs
django-extensions==3.2.1 # db model generation, shellplus
Werkzeug==2.2.3 # for runserver plus by django-extensions
Pillow==9.4.0 # dealing with images
whitenoise[brotli]==6.4.0 # whitenoise for serving staticfiles and using brolti for compression
django-redis==5.2.0 # full featured redis cache backend for Django
drf-yasg==1.21.5 # swagger for docs
django-health-check==3.17.0 # healthcheck
rich==13.3.2 # for visual feedback on scripts
django-cors-headers==3.14.0 # for cors

24
dev.env Normal file
View file

@ -0,0 +1,24 @@
# Django
SECRET_KEY=sample
DEBUG=true
ALLOWED_HOSTS=0.0.0.0,localhost,127.0.0.1,*
CSRF_TRUSTED_ORIGINS=https://foldbank.codingcoffee.me
DJANGO_LOG_LEVEL=INFO
# Django - Postgres
POSTGRES_SERVER=postgres-db
POSTGRES_PORT=5432
POSTGRES_CONN_MAX_AGE=60
POSTGRES_CONN_MAX_AGE_READONLY=60
# RQ
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_QUEUE_DB=0
REDIS_RQ_DEFAULT_TIMEOUT=360
# Postgres, Django
POSTGRES_USER=foldbank
POSTGRES_PASSWORD=password
POSTGRES_DB=foldbank
PGDATA=/var/lib/postgresql/data/pgdata

View file

@ -3,13 +3,46 @@
version: '3.7'
services:
web:
image: node:16.19.1-alpine3.17
# web:
# image: node:16.19.1-alpine3.17
# restart: unless-stopped
# volumes:
# - ./web:/app
# working_dir: /app
# entrypoint: npm run dev
# ports:
# - 3000:3000
backend:
image: foldbank-backend-dev
build:
context: ./backend
dockerfile: Dockerfile.dev
restart: unless-stopped
env_file:
- dev.env
volumes:
- ./backend:/code
working_dir: /code
entrypoint: python manage.py runserver_plus --keep-meta-shutdown 0.0.0.0:8000
depends_on:
- redis
- postgres-db
ports:
- 8000:8000
redis:
image: redis:7.0.0-alpine3.16
restart: unless-stopped
postgres-db:
image: postgres:14.3-alpine3.16
restart: unless-stopped
volumes:
- ./web:/app
working_dir: /app
entrypoint: npm run dev
ports:
- 3000:3000
- postgres-data:/var/lib/postgresql/data/pgdata
env_file:
- dev.env
volumes:
postgres-data: