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

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