Initial commit

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
This commit is contained in:
2020-01-15 20:15:14 -05:00
commit a61ad46b49
45 changed files with 1720 additions and 0 deletions

207
.gitignore vendored Normal file
View File

@@ -0,0 +1,207 @@
# Created by https://www.gitignore.io/api/vim,python,django,virtualenv
# Edit at https://www.gitignore.io/?templates=vim,python,django,virtualenv
### Django ###
*.log
*.pot
*.pyc
__pycache__/
local_settings.py
db.sqlite3
db.sqlite3-journal
media
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
# in your Git repository. Update and uncomment the following line accordingly.
# <django-project-name>/staticfiles/
### Django.Python Stack ###
# Byte-compiled / optimized / DLL files
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
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
.hypothesis/
.pytest_cache/
# Translations
*.mo
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# pyenv
.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
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
### Python ###
# Byte-compiled / optimized / DLL files
# C extensions
# Distribution / packaging
# 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.
# Installer logs
# Unit test / coverage reports
# Translations
# Scrapy stuff:
# Sphinx documentation
# PyBuilder
# pyenv
# 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.
# celery beat schedule file
# SageMath parsed files
# Spyder project settings
# Rope project settings
# Mr Developer
# mkdocs documentation
# mypy
# Pyre type checker
### Vim ###
# Swap
[._]*.s[a-v][a-z]
[._]*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]
# Session
Session.vim
Sessionx.vim
# Temporary
.netrwhist
*~
# Auto-generated tag files
tags
# Persistent undo
[._]*.un~
# Coc configuration directory
.vim
### VirtualEnv ###
# Virtualenv
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
pyvenv.cfg
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
pip-selfcheck.json
# End of https://www.gitignore.io/api/vim,python,django,virtualenv

20
Pipfile Normal file
View File

@@ -0,0 +1,20 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
black = "==19.10b0"
[packages]
django = "==2.2"
django-bootstrap4 = "*"
django-invitations = "*"
django-hashid-field = "*"
frozendict = "*"
[requires]
python_version = "3.8"
[pipenv]
allow_prereleases = true

189
Pipfile.lock generated Normal file
View File

@@ -0,0 +1,189 @@
{
"_meta": {
"hash": {
"sha256": "36bd6ea9711c247710904103a81d2a2abae1000e988ebee11d5eaa22697d5cdf"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.8"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"beautifulsoup4": {
"hashes": [
"sha256:05fd825eb01c290877657a56df4c6e4c311b3965bda790c613a3d6fb01a5462a",
"sha256:9fbb4d6e48ecd30bcacc5b63b94088192dcda178513b2ae3c394229f8911b887",
"sha256:e1505eeed31b0f4ce2dbb3bc8eb256c04cc2b3b72af7d551a4ab6efd5cbe5dae"
],
"version": "==4.8.2"
},
"django": {
"hashes": [
"sha256:7c3543e4fb070d14e10926189a7fcf42ba919263b7473dceaefce34d54e8a119",
"sha256:a2814bffd1f007805b19194eb0b9a331933b82bd5da1c3ba3d7b7ba16e06dc4b"
],
"index": "pypi",
"version": "==2.2"
},
"django-bootstrap4": {
"hashes": [
"sha256:0fcd84f8414a58b43df0b331c00c8b2f1786ae28f75f419b4d33b06fca43e0d1",
"sha256:39f97cbce85eb66f6d76be2029bae171bd3863d0c6932b1c2dae7f299c569b90"
],
"index": "pypi",
"version": "==1.1.1"
},
"django-hashid-field": {
"hashes": [
"sha256:328e83f13ab0eedd4ed8a384bde8b7a4ce18ee8ee1e1d149247ba7611fc7addb",
"sha256:cff0805a4c4243d1c30d180e70efbe08395bddce4177d4c968c1f9bf0542a5a5"
],
"index": "pypi",
"version": "==3.0.0"
},
"django-invitations": {
"hashes": [
"sha256:fe0a4c822bf695f7dac9f828c95f1dcee42dc590ed2943bd4d6ec8a1e03ab08e"
],
"index": "pypi",
"version": "==1.9.2"
},
"frozendict": {
"hashes": [
"sha256:774179f22db2ef8a106e9c38d4d1f8503864603db08de2e33be5b778230f6e45"
],
"index": "pypi",
"version": "==1.2"
},
"hashids": {
"hashes": [
"sha256:6539b892a426e75747a9c0ad69409e9566f9c21b79310fc3424b5b6726f28da6"
],
"version": "==1.2.0"
},
"pytz": {
"hashes": [
"sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
"sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
],
"version": "==2019.3"
},
"soupsieve": {
"hashes": [
"sha256:bdb0d917b03a1369ce964056fc195cfdff8819c40de04695a80bc813c3cfa1f5",
"sha256:e2c1c5dee4a1c36bcb790e0fabd5492d874b8ebd4617622c4f6a731701060dda"
],
"version": "==1.9.5"
},
"sqlparse": {
"hashes": [
"sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177",
"sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873"
],
"version": "==0.3.0"
}
},
"develop": {
"appdirs": {
"hashes": [
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
],
"version": "==1.4.3"
},
"attrs": {
"hashes": [
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
],
"version": "==19.3.0"
},
"black": {
"hashes": [
"sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b",
"sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"
],
"index": "pypi",
"version": "==19.10b0"
},
"click": {
"hashes": [
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
],
"version": "==7.0"
},
"pathspec": {
"hashes": [
"sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424",
"sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96"
],
"version": "==0.7.0"
},
"regex": {
"hashes": [
"sha256:07b39bf943d3d2fe63d46281d8504f8df0ff3fe4c57e13d1656737950e53e525",
"sha256:0932941cdfb3afcbc26cc3bcf7c3f3d73d5a9b9c56955d432dbf8bbc147d4c5b",
"sha256:0e182d2f097ea8549a249040922fa2b92ae28be4be4895933e369a525ba36576",
"sha256:10671601ee06cf4dc1bc0b4805309040bb34c9af423c12c379c83d7895622bb5",
"sha256:23e2c2c0ff50f44877f64780b815b8fd2e003cda9ce817a7fd00dea5600c84a0",
"sha256:26ff99c980f53b3191d8931b199b29d6787c059f2e029b2b0c694343b1708c35",
"sha256:27429b8d74ba683484a06b260b7bb00f312e7c757792628ea251afdbf1434003",
"sha256:3e77409b678b21a056415da3a56abfd7c3ad03da71f3051bbcdb68cf44d3c34d",
"sha256:4e8f02d3d72ca94efc8396f8036c0d3bcc812aefc28ec70f35bb888c74a25161",
"sha256:4eae742636aec40cf7ab98171ab9400393360b97e8f9da67b1867a9ee0889b26",
"sha256:6a6ae17bf8f2d82d1e8858a47757ce389b880083c4ff2498dba17c56e6c103b9",
"sha256:6a6ba91b94427cd49cd27764679024b14a96874e0dc638ae6bdd4b1a3ce97be1",
"sha256:7bcd322935377abcc79bfe5b63c44abd0b29387f267791d566bbb566edfdd146",
"sha256:98b8ed7bb2155e2cbb8b76f627b2fd12cf4b22ab6e14873e8641f266e0fb6d8f",
"sha256:bd25bb7980917e4e70ccccd7e3b5740614f1c408a642c245019cff9d7d1b6149",
"sha256:d0f424328f9822b0323b3b6f2e4b9c90960b24743d220763c7f07071e0778351",
"sha256:d58e4606da2a41659c84baeb3cfa2e4c87a74cec89a1e7c56bee4b956f9d7461",
"sha256:e3cd21cc2840ca67de0bbe4071f79f031c81418deb544ceda93ad75ca1ee9f7b",
"sha256:e6c02171d62ed6972ca8631f6f34fa3281d51db8b326ee397b9c83093a6b7242",
"sha256:e7c7661f7276507bce416eaae22040fd91ca471b5b33c13f8ff21137ed6f248c",
"sha256:ecc6de77df3ef68fee966bb8cb4e067e84d4d1f397d0ef6fce46913663540d77"
],
"version": "==2020.1.8"
},
"toml": {
"hashes": [
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
],
"version": "==0.10.0"
},
"typed-ast": {
"hashes": [
"sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161",
"sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e",
"sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e",
"sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0",
"sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c",
"sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47",
"sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631",
"sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4",
"sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34",
"sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b",
"sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2",
"sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e",
"sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a",
"sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233",
"sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1",
"sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36",
"sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d",
"sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a",
"sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66",
"sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"
],
"version": "==1.4.0"
}
}
}

2
local-manage.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
exec python3 ./manage.py "$@" --settings=market.settings.local

21
manage.py Executable file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'market.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()

0
market/__init__.py Normal file
View File

16
market/asgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
ASGI config for market 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/3.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'market.settings')
application = get_asgi_application()

View File

137
market/settings/common.py Normal file
View File

@@ -0,0 +1,137 @@
"""
Django settings for market project.
Generated by 'django-admin startproject' using Django 3.0.2.
For more information on this file, see
https://docs.djangoproject.com/en/3.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.0/ref/settings/
"""
import os
from pathlib import Path
from django.urls import reverse_lazy
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = str(
Path(__file__) # ./market/settings/common.py
.resolve() # PWD/market/settings/common.py
.parent # PWD/market/settings
.parent # PWD/market/
.parent # PWD/ (goal)
)
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
# SECRET_KEY =
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.humanize",
# Our apps
"trading",
# Third party apps
"bootstrap4",
]
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
]
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 = "market.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 = "market.wsgi.application"
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
}
}
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",},
]
AUTH_USER_MODEL = "trading.User"
LOGIN_URL = reverse_lazy("trading:login")
LOGIN_REDIRECT_URL = reverse_lazy("trading:index")
# Internationalization
# https://docs.djangoproject.com/en/3.0/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_L10N = True
USE_TZ = True
STATIC_URL = "/static/"
# Guardian object permissions can be found at
# https://django-guardian.readthedocs.io/en/stable/configuration.html
#GUARDIAN_RENDER_403 = True
#GUARDIAN_TEMPLATE_403 = "403.html"
# Disable anonymous user permissions
#ANONYMOUS_USER_NAME = None
# TODO: look at this if it makes sense to fetch all user permissions upon application startup
# (Probably not necessary)
# GUARDIAN_AUTO_PREFETCH = True
# This app's settings
from market.settings.defaults import *

View File

@@ -0,0 +1,18 @@
# Whether an invitation is required to join.
TRADING_INVITE_REQUIRED = True
# Number of invites that a user gets when they sign up.
TRADING_FREE_INVITES = 3
# Number of accounts that a new user is allowed to create.
# Use `None` for unlimited.
TRADING_FREE_ACCOUNTS = 5
# Number of commodities that a user gets when they sign up.
TRADING_FREE_COMMODITIES = 1
# Maximum number of commodities allowed for a single IPO
TRADING_IPO_MAX = 5000000
# Minimum number of commodities allowed for a single IPO
TRADING_IPO_MIN = 100

18
market/settings/local.py Normal file
View File

@@ -0,0 +1,18 @@
import os.path
from market.settings.common import *
SECRET_KEY = "65sb@2f)s03(%qcb)cs6-wexn1af&3&$o12pp7%4en_*p6(b-d"
DEBUG = True
ALLOWED_HOSTS = []
TIME_ZONE = "US/Eastern"
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "static"),
]
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
# pwgen -r '"'"'" -cyn 128
HASHID_FIELD_SALT = "h)1qE%eGhOZha_<`*2#$<m^,/K.w;I#s/Er,cpbad-?"
TRADING_FREE_ACCOUNTS = None

28
market/urls.py Normal file
View File

@@ -0,0 +1,28 @@
"""market URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.0/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, include
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("trading.urls")),
]
# Serve up static files using Django server if in DEBUG mode
if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

16
market/wsgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
WSGI config for market 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/3.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'market.settings')
application = get_wsgi_application()

7
static/trading/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

7
static/trading/js/bootstrap.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

5
static/trading/js/popper.min.js vendored Normal file

File diff suppressed because one or more lines are too long

0
tests/__init__.py Normal file
View File

26
tests/test_models.py Normal file
View File

@@ -0,0 +1,26 @@
from django.test import TestCase
from trading.models import User, Commodity, Tx, BalanceError
class TestTransactions(TestCase):
def test_user_balances(self):
# Create users
user1 = User.objects.create_user(username="test1", email="test1@test.test")
user2 = User.objects.create_user(username="test2", email="test2@test.test")
commodity = Commodity(created_by=user1, in_circulation=1000, name="commodity")
commodity.save()
self.assertEqual(user1.balances(), {commodity: 1000})
self.assertEqual(user2.balances(), {})
Tx(source=user1, dest=user2, amount=100, commodity=commodity).save()
self.assertEqual(user1.balances(), {commodity: 900})
self.assertEqual(user2.balances(), {commodity: 100})
with self.assertRaises(BalanceError):
Tx(source=user2, dest=user1, amount=1000, commodity=commodity).save()
self.assertEqual(user1.balances(), {commodity: 900})
self.assertEqual(user2.balances(), {commodity: 100})

0
trading/__init__.py Normal file
View File

3
trading/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
trading/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class TradingConfig(AppConfig):
name = 'trading'

0
trading/forms.py Normal file
View File

31
trading/managers.py Normal file
View File

@@ -0,0 +1,31 @@
from django.contrib.auth.base_user import BaseUserManager
class UserManager(BaseUserManager):
use_in_migrations = True
def _create_user(self, username, email, password, **extra_fields):
"""
Creates and saves a User with the given email and password.
"""
if not email:
raise ValueError('The given email must be set')
if not username:
raise ValueError('The given username must be set')
# TODO: validate username
email = self.normalize_email(email)
user = self.model(username=username, email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, username, email, password=None, **extra_fields):
extra_fields.setdefault('is_superuser', False)
return self._create_user(username, email, password, **extra_fields)
def create_superuser(self, username, email, password, **extra_fields):
extra_fields.setdefault('is_superuser', True)
if extra_fields.get('is_superuser') is not True:
raise ValueError('Superuser must have is_superuser=True.')
return self._create_user(username, email, password, **extra_fields)

View File

@@ -0,0 +1,97 @@
# Generated by Django 2.2 on 2020-01-16 01:11
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import hashid_field.field
import trading.managers
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0011_update_proxy_permissions'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('username', models.CharField(max_length=30, unique=True)),
('email', models.EmailField(max_length=254, unique=True)),
('is_superuser', models.BooleanField(default=False)),
('is_active', models.BooleanField(default=True)),
('date_joined', models.DateTimeField(auto_now_add=True, verbose_name='date joined')),
('max_commodities_allowed', models.PositiveIntegerField(default=1, null=True, verbose_name='number of commodities this user is allowed to create')),
('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={
'verbose_name': 'user',
'verbose_name_plural': 'users',
},
managers=[
('objects', trading.managers.UserManager()),
],
),
migrations.CreateModel(
name='Commodity',
fields=[
('id', hashid_field.field.HashidAutoField(alphabet='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890', min_length=7, primary_key=True, serialize=False)),
('name', models.CharField(max_length=100, unique=True, verbose_name='commodity name')),
('in_circulation', models.PositiveIntegerField()),
('symbol', models.CharField(blank=True, max_length=6, unique=True, verbose_name='symbol')),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'commodity',
'verbose_name_plural': 'commodities',
},
),
migrations.CreateModel(
name='TxRequests',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('status', models.CharField(choices=[('WAIT', 'Waiting'), ('DECL', 'Declined'), ('REV', 'Revised')], default='WAIT', max_length=2)),
('source_amount', models.PositiveIntegerField()),
('dest_amount', models.PositiveIntegerField()),
('dest', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='transaction_requests_received', to=settings.AUTH_USER_MODEL)),
('dest_sends', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='trading.Commodity')),
('previous_request', models.OneToOneField(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='revised_request', to='trading.TxRequests')),
('source', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='transaction_requests_sent', to=settings.AUTH_USER_MODEL)),
('source_sends', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='trading.Commodity')),
],
),
migrations.CreateModel(
name='Tx',
fields=[
('id', hashid_field.field.HashidAutoField(alphabet='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890', min_length=7, primary_key=True, serialize=False)),
('instant', models.DateTimeField(auto_now_add=True)),
('amount', models.PositiveIntegerField(editable=False, verbose_name='amount')),
('commodity', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.PROTECT, to='trading.Commodity')),
('dest', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.PROTECT, related_name='credits', related_query_name='credit_tx', to=settings.AUTH_USER_MODEL, verbose_name='destination')),
('source', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.PROTECT, related_name='debits', related_query_name='debit_tx', to=settings.AUTH_USER_MODEL, verbose_name='source')),
],
options={
'verbose_name': 'transaction',
'verbose_name_plural': 'transactions',
},
),
migrations.CreateModel(
name='Invite',
fields=[
('id', hashid_field.field.HashidAutoField(alphabet='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890', min_length=7, primary_key=True, serialize=False)),
('accepted_user', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='accepted user')),
('inviter', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'invite',
'verbose_name_plural': 'invites',
},
),
]

View File

279
trading/models.py Normal file
View File

@@ -0,0 +1,279 @@
from datetime import date
from typing import Mapping, Optional, Sequence
from django.conf import settings
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.db import models
from django.db.models.signals import post_save, pre_save
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from frozendict import frozendict
from hashid_field import HashidAutoField
from trading.managers import UserManager
class User(PermissionsMixin, AbstractBaseUser):
"""
Base user model, with auth information already filled in.
"""
username = models.CharField(max_length=30, unique=True)
email = models.EmailField(unique=True)
is_superuser = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
date_joined = models.DateTimeField(_("date joined"), auto_now_add=True)
max_commodities_allowed = models.PositiveIntegerField(
default=settings.TRADING_FREE_COMMODITIES,
null=True,
verbose_name=_("number of commodities this user is allowed to create"),
)
objects = UserManager()
USERNAME_FIELD = "username"
EMAIL_FIELD = "email"
REQUIRED_FIELDS = (EMAIL_FIELD,)
class Meta:
verbose_name = _("user")
verbose_name_plural = _("users")
def get_full_name(self):
"""
Returns the first_name plus the last_name, with a space in between.
"""
return self.username
def get_short_name(self):
"""
Returns the short name for the user.
"""
return self.username
def email_user(self, subject, message, from_email=None, **kwargs):
"""
Sends an email to this User.
"""
send_mail(subject, message, from_email, [self.email], **kwargs)
def can_create_commodity(self) -> bool:
if self.max_commodities_allowed is None:
return True
return self.max_commodities_allowed > self.commodity_set.count()
def unused_commodities(self) -> Optional[int]:
if self.max_commodities_allowed is None:
return None
return self.max_commodities_allowed - self.commodity_set.count()
def unused_invites(self) -> Sequence["Invite"]:
return self.invite_set.filter(accepted_user__isnull=True)
def balance_of(self, commodity: "Commodity") -> int:
return self.balances().get(commodity, 0)
def balances(self) -> Mapping["Commodity", int]:
from django.db.models import Sum
credits = {
row["commodity"]: row["amount__sum"]
for row in self.credits.values("commodity").annotate(Sum("amount"))
}
debits = {
row["commodity"]: row["amount__sum"]
for row in self.debits.values("commodity").annotate(Sum("amount"))
}
ipos = {
row["pk"]: row["in_circulation"]
for row in self.commodity_set.values("pk", "in_circulation")
}
keys = set(ipos.keys()) | set(credits.keys()) | set(debits.keys())
return frozendict(
[
(
Commodity.objects.get(pk=pk),
credits.get(pk, 0) + ipos.get(pk, 0) - debits.get(pk, 0),
)
for pk in keys
]
)
@receiver(post_save, sender=User)
def _user_post_save(sender, instance, created, **kwargs):
# Create invites for the user, if they are permitted invites
if created and settings.TRADING_FREE_INVITES:
for _ in range(settings.TRADING_FREE_INVITES):
Invite.objects.create(inviter=instance).save()
class Invite(models.Model):
"""
An invitation that is given out to a user.
"""
id = HashidAutoField(primary_key=True)
inviter = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
accepted_user = models.ForeignKey(
User,
on_delete=models.CASCADE,
null=True,
default=None,
related_name="+",
verbose_name=_("accepted user"),
)
class Meta:
verbose_name = _("invite")
verbose_name_plural = _("invites")
class Commodity(models.Model):
"""
A commodity with some kind of market cap.
A commodity has an associated "owner" account, which determines where the IPO for commodity
goes.
"""
id = HashidAutoField(primary_key=True)
name = models.CharField(
blank=False, unique=True, max_length=100, verbose_name=_("commodity name")
)
in_circulation = models.PositiveIntegerField()
created_by = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
symbol = models.CharField(
blank=True, unique=True, max_length=6, verbose_name=_("symbol")
)
class Meta:
verbose_name = _("commodity")
verbose_name_plural = _("commodities")
def get_absolute_url(self):
return "/c/detail/%s" % self.pk
def html_symbol(self) -> str:
if self.symbol:
return f"<span>{self.symbol}</span>"
else:
return f"<span>units of {self.name}"
class MaxCommodityError(Exception):
def __init__(self, user: User):
self.user = user
super().__init__("maximum commodity count reached")
class Tx(models.Model):
"""
A transaction from one account to another.
"""
id = HashidAutoField(primary_key=True)
instant = models.DateTimeField(auto_now_add=True, editable=False)
source = models.ForeignKey(
User,
on_delete=models.PROTECT,
related_name="debits",
related_query_name="debit_tx",
verbose_name=_("source"),
editable=False,
)
dest = models.ForeignKey(
User,
on_delete=models.PROTECT,
related_name="credits",
related_query_name="credit_tx",
verbose_name=_("destination"),
editable=False,
)
amount = models.PositiveIntegerField(verbose_name=_("amount"), editable=False)
commodity = models.ForeignKey(Commodity, on_delete=models.PROTECT, editable=False)
class Meta:
verbose_name = _("transaction")
verbose_name_plural = _("transactions")
class TxRequests(models.Model):
"""
A transaction request between two users.
"""
WAITING = "WAIT"
DECLINED = "DECL"
REVISED = "REV"
STATUS_CHOICES = [
(WAITING, "Waiting"),
(DECLINED, "Declined"),
(REVISED, "Revised"),
]
status = models.CharField(
max_length=max([len(c) for c in STATUS_CHOICES]),
choices=STATUS_CHOICES,
default=WAITING,
)
previous_request = models.OneToOneField(
"TxRequests",
null=True,
default=None,
on_delete=models.SET_NULL,
related_name="revised_request",
)
source = models.ForeignKey(
User,
on_delete=models.CASCADE,
null=True,
related_name="transaction_requests_sent",
)
dest = models.ForeignKey(
User,
on_delete=models.CASCADE,
null=True,
related_name="transaction_requests_received",
)
source_sends = models.ForeignKey(
Commodity, on_delete=models.CASCADE, related_name="+", null=True,
)
dest_sends = models.ForeignKey(
Commodity, on_delete=models.CASCADE, related_name="+", null=True,
)
source_amount = models.PositiveIntegerField()
dest_amount = models.PositiveIntegerField()
class BalanceError(Exception):
def __init__(
self, balance: int, source: User, dest: User, amount: int, commodity: Commodity
) -> None:
self.balance = balance
self.source = source
self.dest = dest
self.amount = amount
self.commodity = commodity
super().__init__("balance too low to create transaction")
@receiver(pre_save, sender=Tx)
def _tx_pre_save(sender, instance, *args, **kwargs):
"""
Ensures that a transaction cannot be created if the source user doesn't have enough of the given
commodity in their account.
"""
if instance.pk is None:
balance = instance.source.balance_of(instance.commodity)
if instance.amount > balance:
raise BalanceError(
balance,
instance.source,
instance.dest,
instance.amount,
instance.commodity,
)

View File

@@ -0,0 +1,25 @@
{% extends "trading/base.html" %}
{% load bootstrap4 %}
{% block title %}
{% with title="Login" %}
{{ block.super }}
{% endwith %}
{% endblock title %}
{% block content %}
<div class="row">
<div class="col-sm">
<h4>Log in</h4>
<form method="post">
{% csrf_token %}
{% bootstrap_messages %}
{% bootstrap_form form %}
{% buttons submit="Log in" %}{% endbuttons %}
{% if next %}
<input type="hidden" name="next" value="{{next}}"/>
{% endif %}
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,27 @@
{% extends "trading/base.html" %}
{% load bootstrap4 %}
{% block title %}
{% with title="Password change" %}
{{ block.super }}
{% endwith %}
{% endblock title %}
{% block content %}
<div class="row">
<div class="col-sm">
<h4>Update your password</h4>
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
<div class="form-group">
<button class="btn btn-primary" type="submit">Change password</button>
<a class="btn btn-link" href="{% url "trading:index" %}" role="button">Cancel</a>
</div>
{% if next %}
<input type="hidden" name="next" value="{{next}}"/>
{% endif %}
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,27 @@
{% extends "trading/base.html" %}
{% block title %}
{% with title="Password change - success" %}
{{ block.super }}
{% endwith %}
{% endblock title %}
{% block content %}
<div class="row">
<div class="col-sm">
<h2>Your password has been changed.</h2>
<p>
You will be redirected in 3 seconds.
<a href="{{next|default:"/"}}" target="_blank">Click here if you are not automatically redirected.</a>
</p>
</div>
</div>
<script type="text/javascript">
setTimeout(function() {
document.location = "{{next|default:"/"}}";
}, 3000);
</script>
{% endblock %}

View File

@@ -0,0 +1,24 @@
{% extends "trading/base.html" %}
{% load bootstrap4 %}
{% block title %}
{% with title="Password reset" %}
{{ block.super }}
{% endwith %}
{% endblock title %}
{% block content %}
<div class="row">
<div class="col-sm">
<h4 class="text-center">Reset your password</h4>
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons submit="Reset password" %}{% endbuttons %}
{% if next %}
<input type="hidden" name="next" value="{{next}}"/>
{% endif %}
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,17 @@
{% extends "trading/base.html" %}
{% block title %}
{% with title="Password reset" %}
{{ block.super }}
{% endwith %}
{% endblock title %}
{% block content %}
<div class="row">
<div class="col-sm">
<h4>Success</h4>
Your password has successfully been reset. You may now use your new password to
<a href="{% url 'trading:login'%}"> log in</a>.
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,34 @@
{% extends "trading/base.html" %}
{% load bootstrap4 %}
{% block title %}
{% with title="Reset" %}
{{ block.super }}
{% endwith %}
{% endblock title %}
{% block content %}
<div class="row">
<div class="col-sm">
{% if form %}
<h4 class="text-center">Reset your password</h4>
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons submit="Reset password" %}{% endbuttons %}
{% if next %}
<input type="hidden" name="next" value="{{next}}"/>
{% endif %}
</form>
{% else %}
<h4 class="text-center">Password reset error</h4>
<p>
The password reset link you are trying to use is either invalid
or has expired. If you need to reset your password,
<a href="{% url 'trading:password_reset' %}">click here</a> to
do so.
</p>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,20 @@
{% extends "trading/base.html" %}
{% block title %}
{% with title="Password reset" %}
{{ block.super }}
{% endwith %}
{% endblock title %}
{% block content %}
<div class="row">
<div class="col-sm">
<h4>Check your email</h4>
<p>
A password reset link has been sent to your email address. Please check your email
and follow the instructions to finish resetting your password.
</p>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,75 @@
{% extends "trading/base.html" %}
{% load bootstrap4 %}
{% block title %}
{% with title="Settings" %}
{{ block.super }}
{% endwith %}
{% endblock title %}
{% block content %}
<div class="row">
<div class="col-sm">
<h2>User settings</h2>
<strong> (<a href="{% url "trading:user_profile" object.id %}">view profile</a>)
</div>
</div>
<div class="row">
<div class="col-sm">
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
<div class="form-group">
<button class="btn btn-primary" type="submit">Update</button>
</div>
{% if next %}
<input type="hidden" name="next" value="{{next}}"/>
{% endif %}
</form>
<p>
<a href="{% url "trading:password_change" %}">Change your password here</a>
</p>
</div>
<div class="col-sm">
<h4>Invites</h4>
{% if object.unused_invites %}
<table class="table">
<thead>
<tr>
<th>Invite #</th>
<th>Link</th>
</tr>
</thead>
<tbody>
{% for invite in object.invite_set.all %}
{% if not invite.accepted_user %}
<tr>
<td>{{ forloop.counter }}</td>
<td>TODO Copy link script</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
{% else %}
No invites available.
{% endif %}
</div>
</div>
{% endblock %}
{% comment %}
<script>
const copyToClipboard = str => {
const el = document.createElement('textarea');
el.value = str;
el.setAttribute('readonly', '');
el.style.position = 'absolute';
el.style.left = '-9999px';
document.body.appendChild(el);
el.select();
document.execCommand('copy');
document.body.removeChild(el);
};
</script>
{% endcomment %}

View File

@@ -0,0 +1,34 @@
{% load staticfiles %}
{% load bootstrap4 %}
{# TODO l8n #}
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="{% static 'trading/css/bootstrap.min.css' %}" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T">
<title>{% block title %}Trading{% if title %} - {{ title }}{% endif %}{% endblock title %}</title>
</head>
<body>
{% block navbar %}
{% include "trading/navbar.html" %}
{% endblock navbar %}
<main>
<div class="container mt-5"> <!-- -->
{% block messages %}
{% bootstrap_messages %}
{% endblock %}
{% block content %}{% endblock %}
</div>
</main>
<script src="{% static 'trading/js/jquery-3.3.1.slim.min.js' %}" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"></script>
<script src="{% static 'trading/js/popper.min.js' %}" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1"></script>
<script src="{% static 'trading/js/bootstrap.min.js' %}" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"></script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,18 @@
{% load humanize %}
<table class="table">
<thead>
<tr>
<th>Commodity</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
{% for commodity, balance in object.balances.items %}
<tr>
<td>{{commodity.name}}</td>
<td>{{balance|intcomma}}</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@@ -0,0 +1,40 @@
{% extends "trading/base.html" %}
{% load bootstrap4 %}
{% block title %}
{% with title="Create commodity" %}
{{ block.super }}
{% endwith %}
{% endblock title %}
{% block content %}
<div class="row">
<div class="col-sm">
<h4>Create commodity</h4>
{% if object.can_create_commodity %}
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
<div class="form-group">
<div class="alert alert-light">
<strong>Note:</strong> You have <strong>{{ object.unused_commodities|default:"unlimited" }}</strong>
{% if object.unused_commodities %}
commodit{{ object.unused_commodities|pluralize:"y,ies"}}
{% else %}
commodities
{% endif %} available to create.
</div>
<button class="btn btn-primary" type="submit">Create</button>
<a class="btn btn-link" href="{% url "trading:index" %}" role="button">Cancel</a>
</div>
{% if next %}
<input type="hidden" name="next" value="{{next}}"/>
{% endif %}
</form>
{% else %}
<p>You have created the maximum number of commodities you are allowed to create.</p>
<p><a href="{% url "trading:settings" %}">Click here to view your settings.</a></p>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,16 @@
{% extends "trading/base.html" %}
{% load bootstrap4 %}
{% block title %}
{% with title="Commodity detail" %}
{{ block.super }}
{% endwith %}
{% endblock title %}
{% block content %}
<div class="row">
<div class="col-sm">
<h4>{{object.name}}</h4>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% extends "trading/base.html" %}
{% block content %}
<div class="row">
<div class="col-sm">
<h4>Balances</h4>
<p>{% include "trading/c/balances.html" with object=request.user %}</p>
{% if request.user.can_create_commodity %}
<p><a href="{% url "trading:commodity_create" %}">Create a new commodity</a></p>
{% endif %}
</div>
<div class="col-sm">
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,3 @@
{% load staticfiles %}
<nav class="navbar navbar-expand-lg">
</nav>

View File

@@ -0,0 +1,51 @@
{% extends "trading/base.html" %}
{% load bootstrap4 %}
{% load humanize %}
{% block title %}
{% with title="Profile" %}
{{ block.super }}
{% endwith %}
{% endblock title %}
{% block content %}
<div class="row">
<div class="col-sm">
<h2>{{ object.username }}</h2>
</div>
</div>
<div class="row">
<div class="col-sm">
<table class="table">
<tr>
<td>Status</td>
<td>{{ object_status }}</td>
</tr>
<tr>
<td>Date joined</td>
<td>{{ object.date_joined.date }}</td>
</tr>
</table>
</div>
<div class="col-sm">
<h4>Commodities owned by this user</h4>
<table class="table">
<thead>
<tr>
<th>Commodity</th>
<th>Circulation</th>
</tr>
</thead>
<tbody>
{% for commodity in object.commodity_set.all %}
<tr>
<td><a href="{% url "trading:commodity_detail" commodity.pk %}">{{ commodity.name }}</a></td>
<td>{{ commodity.in_circulation|intcomma }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

3
trading/tests.py Normal file
View File

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

77
trading/urls.py Normal file
View File

@@ -0,0 +1,77 @@
from django.urls import path, include
from django.views.generic.base import RedirectView
from django.contrib.auth.views import *
from django.urls import reverse_lazy
from trading.views import *
app_name = "trading"
urlpatterns = [
path("", IndexView.as_view(), name="index"),
path("login/", RedirectView.as_view(url=reverse_lazy("trading:login"))),
path("logout/", RedirectView.as_view(url=reverse_lazy("trading:logout"))),
path(
"accounts/login/",
LoginView.as_view(template_name="trading/account/login.html",),
name="login",
),
path("accounts/logout/", LogoutView.as_view(), name="logout"),
path(
"accounts/password_reset/",
PasswordResetView.as_view(
template_name="trading/account/password_reset.html",
success_url=reverse_lazy("trading:password_reset_done"),
),
name="password_reset",
),
path(
"accounts/password_reset_done/",
PasswordResetDoneView.as_view(
template_name="trading/account/password_reset_done.html"
),
name="password_reset_done",
),
path(
"accounts/password_reset_confirm/",
PasswordResetConfirmView.as_view(
template_name="trading/account/password_reset_confirm.html",
success_url=reverse_lazy("trading:password_reset_complete"),
),
name="password_reset_confirm",
),
path(
"accounts/password_reset_complete/",
PasswordResetCompleteView.as_view(
template_name="trading/account/password_reset_complete.html"
),
name="password_reset_complete",
),
path(
"accounts/password_change/",
PasswordChangeView.as_view(
template_name="trading/account/password_change.html",
success_url=reverse_lazy("trading:password_change_done"),
),
name="password_change",
),
path(
"accounts/password_change_done/",
PasswordChangeDoneView.as_view(
template_name="trading/account/password_change_done.html"
),
name="password_change_done",
),
path("accounts/settings/", UserSettingsView.as_view(), name="settings"),
# u/ for users
path("u/profile/<int:pk>/", UserProfileView.as_view(), name="user_profile"),
# t/ for tx
# c/ for commodities
path("c/create/", CommodityCreateView.as_view(), name="commodity_create"),
#path("c/list/", CommodityListView.as_view(), name="commodity_list"),
path("c/detail/<str:pk>/", CommodityDetailView.as_view(), name="commodity_detail"),
]

80
trading/views.py Normal file
View File

@@ -0,0 +1,80 @@
from django import forms
from django.contrib.auth import views as auth_views, password_validation
from django.contrib.auth.forms import UsernameField
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.http import HttpResponseForbidden, HttpResponseRedirect
from django.views.generic import TemplateView
from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView, UpdateView
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
from trading.models import User, Commodity, MaxCommodityError
class IndexView(LoginRequiredMixin, TemplateView):
template_name = "trading/index.html"
class CommodityDetailView(DetailView):
template_name = "trading/c/detail.html"
model = Commodity
class CommodityCreateView(LoginRequiredMixin, CreateView):
template_name = "trading/c/create.html"
model = Commodity
fields = (
"name",
"in_circulation",
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["object"] = self.request.user
return context
def form_valid(self, form):
user = self.request.user
if not user.can_create_commodity():
raise MaxCommodityError(user)
form.instance.created_by = user
return super(CreateView, self).form_valid(form)
class UserProfileView(DetailView):
template_name = "trading/u/profile.html"
model = User
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.object.is_active:
status = "Active"
else:
status = "Inactive"
context["object_status"] = status
return context
class UserUpdateSettingsForm(forms.ModelForm):
class Meta:
model = User
fields = (
"username",
"email",
)
field_classes = {"username": UsernameField}
def __init__(self, user, *args, **kwargs):
super().__init__(*args, **kwargs)
class UserSettingsView(LoginRequiredMixin, UpdateView):
template_name = "trading/account/settings.html"
model = User
fields = ("username", "email")
success_url = reverse_lazy("trading:settings")
def get_object(self, queryset=None):
return self.request.user