migrate to django 4 #1

Open
informaniac wants to merge 4 commits from migrate/django4 into master
  1. 9
      .gitignore
  2. 6
      cashonly/core/__init__.py
  3. 108
      cashonly/core/admin.py
  4. 5
      cashonly/core/apps.py
  5. 4
      cashonly/core/auth.py
  6. 4
      cashonly/core/formfields.py
  7. 89
      cashonly/core/management/commands/dailydigest.py
  8. 31
      cashonly/core/management/commands/debtreminder.py
  9. 270
      cashonly/core/migrations/0001_initial.py
  10. 10
      cashonly/core/migrations/0002_account_debit_limit.py
  11. 15
      cashonly/core/migrations/0003_debit_limit_null_default.py
  12. 10
      cashonly/core/migrations/0004_account_avatar.py
  13. 103
      cashonly/core/models.py
  14. 53
      cashonly/core/services.py
  15. 6
      cashonly/web/__init__.py
  16. 2
      cashonly/web/templates/cashonly/web/base.html
  17. 2
      cashonly/web/templates/cashonly/web/login.html
  18. 2
      cashonly/web/templates/cashonly/web/usersettings.html
  19. 61
      cashonly/web/urls.py
  20. 165
      cashonly/web/views.py
  21. 4
      config/copysecret.py
  22. 92
      config/settings.py
  23. 14
      config/urls.py
  24. 2
      requirements.txt

9
.gitignore vendored

@ -1,4 +1,11 @@
venv/ venv/
online/ online/
.idea/ .idea/
.DS_Store .DS_Store
# local files
server/
cashonly/web/static/bootstrap/**/*
cashonly/web/static/jquery.min.js
db.sqlite3
manage.py

6
cashonly/core/__init__.py

@ -2,11 +2,11 @@ from django.apps import AppConfig
class CashonlyCoreAppConfig(AppConfig): class CashonlyCoreAppConfig(AppConfig):
name = 'cashonly.core' name = "cashonly.core"
label = 'cashonly_core' label = "cashonly_core"
def ready(self): def ready(self):
from . import services from . import services
default_app_config = 'cashonly.core.CashonlyCoreAppConfig' default_app_config = "cashonly.core.CashonlyCoreAppConfig"

108
cashonly/core/admin.py

@ -3,26 +3,23 @@ from cashonly.core.models import *
from cashonly.core.formfields import DigitField from cashonly.core.formfields import DigitField
from cashonly.core.services import AccountManager from cashonly.core.services import AccountManager
from django import forms from django import forms
from django.template.defaultfilters import escape from django.urls import reverse
from django.core.urlresolvers import reverse from django.utils.translation import gettext as _
from django.utils.translation import ugettext as _ from django.utils.translation import gettext_lazy
from django.utils.translation import ugettext_lazy
from django.utils.translation import ugettext_noop
class AccountForm(forms.ModelForm): class AccountForm(forms.ModelForm):
credit_change = forms.DecimalField(max_digits=5, decimal_places=2, credit_change = forms.DecimalField(
required=False, max_digits=5, decimal_places=2, required=False, label=gettext_lazy("amount")
label=ugettext_lazy('amount')) )
credit_change_comment = forms.CharField(max_length=64, required=False, credit_change_comment = forms.CharField(
label=ugettext_lazy('comment')) max_length=64, required=False, label=gettext_lazy("comment")
)
pin_change = DigitField(min_length=4, required=False, pin_change = DigitField(min_length=4, required=False, label=gettext_lazy("PIN"))
label=ugettext_lazy('PIN'))
pin_empty = forms.BooleanField(required=False, pin_empty = forms.BooleanField(required=False, label=gettext_lazy("clear PIN"))
label=ugettext_lazy('clear PIN'))
class Meta: class Meta:
model = Account model = Account
@ -32,21 +29,31 @@ class AccountForm(forms.ModelForm):
class AccountAdmin(admin.ModelAdmin): class AccountAdmin(admin.ModelAdmin):
list_display = ('user', 'card_number', 'credit', 'debit_limit', list_display = ("user", "card_number", "credit", "debit_limit", "transaction_link")
'transaction_link')
form = AccountForm form = AccountForm
readonly_fields = ('user', 'credit',) readonly_fields = (
"user",
"credit",
)
fieldsets = ( fieldsets = (
(None, { (
'fields': ('user', 'card_number', 'credit', 'debit_limit', None,
'avatar'), {
}), "fields": ("user", "card_number", "credit", "debit_limit", "avatar"),
(ugettext_lazy('credit change'), { },
'fields': ('credit_change', 'credit_change_comment'), ),
}), (
(ugettext_lazy('change PIN'), { gettext_lazy("credit change"),
'fields': ('pin_change', 'pin_empty'), {
}), "fields": ("credit_change", "credit_change_comment"),
},
),
(
gettext_lazy("change PIN"),
{
"fields": ("pin_change", "pin_empty"),
},
),
) )
# Disable manual creation of accounts. # Disable manual creation of accounts.
@ -55,26 +62,28 @@ class AccountAdmin(admin.ModelAdmin):
return False return False
def transaction_link(self, account): def transaction_link(self, account):
return '<a href="%s?account__id__exact=%d">%s</a>' % \ return '<a href="%s?account__id__exact=%d">%s</a>' % (
(reverse("admin:cashonly_core_transaction_changelist"), account.id, reverse("admin:cashonly_core_transaction_changelist"),
_('Transactions')) account.id,
_("Transactions"),
)
transaction_link.short_description = ugettext_lazy('Transaction link') transaction_link.short_description = gettext_lazy("Transaction link")
transaction_link.allow_tags = True transaction_link.allow_tags = True
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
accmgr = AccountManager(obj) accmgr = AccountManager(obj)
pin = form.cleaned_data['pin_change'] pin = form.cleaned_data["pin_change"]
pin_empty = form.cleaned_data['pin_empty'] pin_empty = form.cleaned_data["pin_empty"]
if pin_empty: if pin_empty:
accmgr.clear_pin() accmgr.clear_pin()
elif pin is not None and len(pin) != 0: elif pin is not None and len(pin) != 0:
accmgr.set_pin(pin) accmgr.set_pin(pin)
amount = form.cleaned_data['credit_change'] amount = form.cleaned_data["credit_change"]
comment = form.cleaned_data['credit_change_comment'] comment = form.cleaned_data["credit_change_comment"]
if amount is not None and amount != 0: if amount is not None and amount != 0:
accmgr.change_credit(amount, request.user, comment) accmgr.change_credit(amount, request.user, comment)
@ -89,32 +98,32 @@ class ProductBarcodeInline(admin.TabularInline):
class ProductAdmin(admin.ModelAdmin): class ProductAdmin(admin.ModelAdmin):
list_display = ('name', 'category', 'price', 'active') list_display = ("name", "category", "price", "active")
list_filter = ['category', 'active'] list_filter = ["category", "active"]
ordering = ('-active', 'name') ordering = ("-active", "name")
inlines = [ProductBarcodeInline] inlines = [ProductBarcodeInline]
fields = ('name', 'price', 'active', 'category', 'image') fields = ("name", "price", "active", "category", "image")
class ProductCategoryAdmin(admin.ModelAdmin): class ProductCategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'comment') list_display = ("name", "comment")
class SalesLogEntryAdmin(admin.ModelAdmin): class SalesLogEntryAdmin(admin.ModelAdmin):
list_display = ('account', 'timestamp', 'product', 'count', 'unit_price') list_display = ("account", "timestamp", "product", "count", "unit_price")
list_filter = ['account', 'timestamp', 'product'] list_filter = ["account", "timestamp", "product"]
# Make sales log entries completely read-only # Make sales log entries completely read-only
readonly_fields = list(map(lambda f: f.name, SalesLogEntry._meta.fields)) readonly_fields = list(map(lambda f: f.name, SalesLogEntry._meta.fields))
class TransactionAdmin(admin.ModelAdmin): class TransactionAdmin(admin.ModelAdmin):
list_display = ('account', 'timestamp', 'subject', 'description', 'amount') list_display = ("account", "timestamp", "subject", "description", "amount")
list_filter = ['account', 'timestamp', 'subject'] list_filter = ["account", "timestamp", "subject"]
# Disable mass deletion in the overview page # Disable mass deletion in the overview page
actions = None actions = None
date_hierarchy = 'timestamp' date_hierarchy = "timestamp"
# Disable tampering with the transactions completely. # Disable tampering with the transactions completely.
def has_add_permission(self, request): def has_add_permission(self, request):
@ -131,11 +140,12 @@ class TransactionAdmin(admin.ModelAdmin):
# Needed to not trigger an ImproperlyConfigured exception. # Needed to not trigger an ImproperlyConfigured exception.
# FIXME: a bit too hacky # FIXME: a bit too hacky
def changelist_view(self, request, extra_context=None): def changelist_view(self, request, extra_context=None):
self.list_display_links = (None, ) self.list_display_links = (None,)
return super(TransactionAdmin, self).changelist_view( return super(TransactionAdmin, self).changelist_view(
request, request,
extra_context=None, extra_context=None,
) )
admin.site.register(Account, AccountAdmin) admin.site.register(Account, AccountAdmin)
admin.site.register(Product, ProductAdmin) admin.site.register(Product, ProductAdmin)

5
cashonly/core/apps.py

@ -2,5 +2,6 @@ from django.apps import AppConfig
class CashonlyCoreConfig(AppConfig): class CashonlyCoreConfig(AppConfig):
name = 'cashonly.core' name = "cashonly.core"
label = 'cashonly_core' label = "cashonly_core"
default_auto_field = "django.db.models.AutoField"

4
cashonly/core/auth.py

@ -6,9 +6,9 @@ from cashonly.core.services import AccountManager
class UsernameCardnumberPinBackend(object): class UsernameCardnumberPinBackend(object):
def authenticate(self, username=None, card_number=None, pin=None): def authenticate(self, username=None, card_number=None, pin=None):
if username is not None and card_number is not None: if username is not None and card_number is not None:
raise ValueError('username and card_number are mutually exclusive') raise ValueError("username and card_number are mutually exclusive")
if username is None and card_number is None: if username is None and card_number is None:
raise ValueError('either username and card_number is required') raise ValueError("either username and card_number is required")
try: try:
if username is not None: if username is not None:

4
cashonly/core/formfields.py

@ -1,6 +1,6 @@
from django.forms import CharField from django.forms import CharField
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.core.validators import EMPTY_VALUES from django.core.validators import EMPTY_VALUES
@ -9,6 +9,6 @@ class DigitField(CharField):
super(DigitField, self).clean(value) super(DigitField, self).clean(value)
if value not in EMPTY_VALUES and not value.isdigit(): if value not in EMPTY_VALUES and not value.isdigit():
raise ValidationError(_('Please enter only digits.')) raise ValidationError(_("Please enter only digits."))
return value return value

89
cashonly/core/management/commands/dailydigest.py

@ -1,75 +1,90 @@
from cashonly.core.models import * from cashonly.core.models import *
from django.conf import settings from django.conf import settings
from django.core.mail import send_mass_mail from django.core.mail import send_mass_mail
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand
from django.template import Context
from django.template.loader import get_template from django.template.loader import get_template
from django.utils import translation from django.utils import translation
from django.utils.dateformat import DateFormat from django.utils.dateformat import DateFormat
from django.utils.formats import get_format from django.utils.formats import get_format
from django.utils.translation import ugettext as _ from django.utils.translation import gettext as _
import datetime import datetime
class Command(BaseCommand): class Command(BaseCommand):
help = 'Sends out a digest of transactions to recently active users' help = "Sends out a digest of transactions to recently active users"
def handle(self, **options): def handle(self, **options):
translation.activate('de') translation.activate("de")
tpl = get_template('cashonly/core/daily_digest.txt') tpl = get_template("cashonly/core/daily_digest.txt")
messages = [] messages = []
for a in Account.objects.filter(daily_digest=True): for a in Account.objects.filter(daily_digest=True):
name = '%s %s' % (a.user.first_name, a.user.last_name) name = "%s %s" % (a.user.first_name, a.user.last_name)
context = { context = {
'name': name, "name": name,
'credit': a.credit, "credit": a.credit,
'range': settings.CASHONLY_DAILY_DIGEST_RANGE_HOURS, "range": settings.CASHONLY_DAILY_DIGEST_RANGE_HOURS,
'url': settings.CASHONLY_USERSETTINGS_URL, "url": settings.CASHONLY_USERSETTINGS_URL,
} }
min_ts = datetime.datetime.now() - datetime.timedelta( min_ts = datetime.datetime.now() - datetime.timedelta(
hours=settings.CASHONLY_DAILY_DIGEST_RANGE_HOURS hours=settings.CASHONLY_DAILY_DIGEST_RANGE_HOURS
) )
transactions = Transaction.objects.filter(account=a) \ transactions = Transaction.objects.filter(account=a).filter(
.filter(timestamp__gte=min_ts) timestamp__gte=min_ts
)
if transactions.count() > 0: if transactions.count() > 0:
lengths = {'timestamp': len(_('date')), lengths = {
'description': len(_('subject')), "timestamp": len(_("date")),
'amount': len(_('amount'))} "description": len(_("subject")),
"amount": len(_("amount")),
}
sum = 0 sum = 0
for t in transactions: for t in transactions:
lengths['timestamp'] = \ lengths["timestamp"] = max(
max(lengths['timestamp'], len(DateFormat(t.timestamp) lengths["timestamp"],
.format(get_format('SHORT_DATETIME_FORMAT')))) len(
lengths['description'] = \ DateFormat(t.timestamp).format(
max(lengths['description'], len(t.description)) get_format("SHORT_DATETIME_FORMAT")
lengths['amount'] = \ )
max(lengths['amount'], len(str(t.amount))) ),
t.description = t.description.split('\n') )
lengths["description"] = max(
lengths["description"], len(t.description)
)
lengths["amount"] = max(lengths["amount"], len(str(t.amount)))
t.description = t.description.split("\n")
sum += t.amount sum += t.amount
lengths['sum'] = lengths['timestamp'] + \ lengths["sum"] = (
lengths['description'] + lengths['amount'] lengths["timestamp"] + lengths["description"] + lengths["amount"]
context['lengths'] = lengths )
context['tl'] = transactions context["lengths"] = lengths
context['sum'] = sum context["tl"] = transactions
context["sum"] = sum
if a.user.email is not None and len(a.user.email) > 0: if a.user.email is not None and len(a.user.email) > 0:
rcpts = ['%s <%s>' % (name, a.user.email)] rcpts = ["%s <%s>" % (name, a.user.email)]
else: else:
self.stdout.write(self.style.WARNING( self.stdout.write(
'User "%s" has no Email address.' % (a.user.username) self.style.WARNING(
)) 'User "%s" has no Email address.' % (a.user.username)
)
)
continue continue
messages.append(('%s%s' % (settings.EMAIL_SUBJECT_PREFIX, messages.append(
_('Account Statement')), (
tpl.render(context), "%s%s"
settings.DEFAULT_FROM_EMAIL, rcpts)) % (settings.EMAIL_SUBJECT_PREFIX, _("Account Statement")),
tpl.render(context),
settings.DEFAULT_FROM_EMAIL,
rcpts,
)
)
send_mass_mail(tuple(messages)) send_mass_mail(tuple(messages))

31
cashonly/core/management/commands/debtreminder.py

@ -1,4 +1,3 @@
from cashonly.models import * from cashonly.models import *
from django.conf import settings from django.conf import settings
from django.core.mail import send_mass_mail from django.core.mail import send_mass_mail
@ -6,28 +5,32 @@ from django.core.management.base import NoArgsCommand
from django.template import Context from django.template import Context
from django.template.loader import get_template from django.template.loader import get_template
from django.utils import translation from django.utils import translation
from django.utils.translation import ugettext as _ from django.utils.translation import gettext as _
class Command(NoArgsCommand): class Command(NoArgsCommand):
help = 'Sends a reminder mail to every with a negative credit' help = "Sends a reminder mail to every with a negative credit"
def handle_noargs(self, **options): def handle_noargs(self, **options):
translation.activate('de') translation.activate("de")
tpl = get_template('cashonly/debt_reminder.txt') tpl = get_template("cashonly/debt_reminder.txt")
messages = [] messages = []
for a in Account.objects.all(): for a in Account.objects.all():
if a.credit < 0: if a.credit < 0:
name = '%s %s' % (a.user.first_name, a.user.last_name) name = "%s %s" % (a.user.first_name, a.user.last_name)
context = {'name': name, 'credit': a.credit} context = {"name": name, "credit": a.credit}
rcpts = ['%s <%s>' % (name, a.user.email)] rcpts = ["%s <%s>" % (name, a.user.email)]
messages.append(('%s%s' % (settings.EMAIL_SUBJECT_PREFIX, messages.append(
_('Debt Reminder')), (
tpl.render(Context(context)), "%s%s" % (settings.EMAIL_SUBJECT_PREFIX, _("Debt Reminder")),
settings.DEFAULT_FROM_EMAIL, rcpts)) tpl.render(Context(context)),
settings.DEFAULT_FROM_EMAIL,
rcpts,
)
)
send_mass_mail(tuple(messages)) send_mass_mail(tuple(messages))

270
cashonly/core/migrations/0001_initial.py

@ -17,93 +17,259 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Account', name="Account",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('card_number', models.CharField(blank=True, max_length=32, null=True, unique=True, verbose_name='card number')), "id",
('pin', models.CharField(blank=True, max_length=32, verbose_name='PIN')), models.AutoField(
('daily_digest', models.BooleanField(default=True, verbose_name='daily digest')), auto_created=True,
('credit', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='credit')), primary_key=True,
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), serialize=False,
verbose_name="ID",
),
),
(
"card_number",
models.CharField(
blank=True,
max_length=32,
null=True,
unique=True,
verbose_name="card number",
),
),
(
"pin",
models.CharField(blank=True, max_length=32, verbose_name="PIN"),
),
(
"daily_digest",
models.BooleanField(default=True, verbose_name="daily digest"),
),
(
"credit",
models.DecimalField(
decimal_places=2, default=0, max_digits=5, verbose_name="credit"
),
),
(
"user",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
], ],
options={ options={
'verbose_name': 'account', "verbose_name": "account",
'verbose_name_plural': 'accounts', "verbose_name_plural": "accounts",
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='Product', name="Product",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('name', models.CharField(max_length=32, unique=True, verbose_name='name')), "id",
('price', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='price')), models.AutoField(
('active', models.BooleanField(default=True, verbose_name='active')), auto_created=True,
('image', models.ImageField(blank=True, null=True, upload_to='products', verbose_name='image')), primary_key=True,
('image_thumbnail', models.ImageField(blank=True, null=True, upload_to='products_thumb', verbose_name='image')), serialize=False,
verbose_name="ID",
),
),
(
"name",
models.CharField(max_length=32, unique=True, verbose_name="name"),
),
(
"price",
models.DecimalField(
decimal_places=2, max_digits=5, verbose_name="price"
),
),
("active", models.BooleanField(default=True, verbose_name="active")),
(
"image",
models.ImageField(
blank=True,
null=True,
upload_to="products",
verbose_name="image",
),
),
(
"image_thumbnail",
models.ImageField(
blank=True,
null=True,
upload_to="products_thumb",
verbose_name="image",
),
),
], ],
options={ options={
'verbose_name': 'product', "verbose_name": "product",
'verbose_name_plural': 'products', "verbose_name_plural": "products",
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='ProductBarcode', name="ProductBarcode",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('barcode', models.CharField(max_length=32, unique=True, verbose_name='barcode')), "id",
('comment', models.CharField(blank=True, max_length=128, verbose_name='comment')), models.AutoField(
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cashonly_core.Product', verbose_name='product')), auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"barcode",
models.CharField(
max_length=32, unique=True, verbose_name="barcode"
),
),
(
"comment",
models.CharField(
blank=True, max_length=128, verbose_name="comment"
),
),
(
"product",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="cashonly_core.Product",
verbose_name="product",
),
),
], ],
options={ options={
'verbose_name': 'barcode', "verbose_name": "barcode",
'verbose_name_plural': 'barcodes', "verbose_name_plural": "barcodes",
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='ProductCategory', name="ProductCategory",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('name', models.CharField(max_length=32, unique=True, verbose_name='name')), "id",
('comment', models.CharField(blank=True, max_length=128, verbose_name='comment')), models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"name",
models.CharField(max_length=32, unique=True, verbose_name="name"),
),
(
"comment",
models.CharField(
blank=True, max_length=128, verbose_name="comment"
),
),
], ],
options={ options={
'verbose_name': 'product category', "verbose_name": "product category",
'verbose_name_plural': 'product categories', "verbose_name_plural": "product categories",
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='SalesLogEntry', name="SalesLogEntry",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('count', models.IntegerField(verbose_name='count')), "id",
('unit_price', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='unit price')), models.AutoField(
('timestamp', models.DateTimeField(auto_now_add=True, verbose_name='timestamp')), auto_created=True,
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cashonly_core.Account', verbose_name='account')), primary_key=True,
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cashonly_core.Product', verbose_name='product')), serialize=False,
verbose_name="ID",
),
),
("count", models.IntegerField(verbose_name="count")),
(
"unit_price",
models.DecimalField(
decimal_places=2, max_digits=5, verbose_name="unit price"
),
),
(
"timestamp",
models.DateTimeField(auto_now_add=True, verbose_name="timestamp"),
),
(
"account",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="cashonly_core.Account",
verbose_name="account",
),
),
(
"product",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="cashonly_core.Product",
verbose_name="product",
),
),
], ],
options={ options={
'verbose_name': 'sales log entry', "verbose_name": "sales log entry",
'verbose_name_plural': 'sales log entries', "verbose_name_plural": "sales log entries",
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='Transaction', name="Transaction",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('timestamp', models.DateTimeField(auto_now_add=True, verbose_name='timestamp')), "id",
('subject', models.CharField(max_length=32, verbose_name='subject')), models.AutoField(
('description', models.TextField(verbose_name='description')), auto_created=True,
('amount', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='amount')), primary_key=True,
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cashonly_core.Account', verbose_name='account')), serialize=False,
verbose_name="ID",
),
),
(
"timestamp",
models.DateTimeField(auto_now_add=True, verbose_name="timestamp"),
),
("subject", models.CharField(max_length=32, verbose_name="subject")),
("description", models.TextField(verbose_name="description")),
(
"amount",
models.DecimalField(
decimal_places=2, max_digits=5, verbose_name="amount"
),
),
(
"account",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="cashonly_core.Account",
verbose_name="account",
),
),
], ],
options={ options={
'verbose_name': 'transaction', "verbose_name": "transaction",
'verbose_name_plural': 'transactions', "verbose_name_plural": "transactions",
}, },
), ),
migrations.AddField( migrations.AddField(
model_name='product', model_name="product",
name='category', name="category",
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='cashonly_core.ProductCategory', verbose_name='category'), field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="cashonly_core.ProductCategory",
verbose_name="category",
),
), ),
] ]

10
cashonly/core/migrations/0002_account_debit_limit.py

@ -8,13 +8,15 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('cashonly_core', '0001_initial'), ("cashonly_core", "0001_initial"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='account', model_name="account",
name='debit_limit', name="debit_limit",
field=models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='Debit Limit'), field=models.DecimalField(
decimal_places=2, default=0, max_digits=5, verbose_name="Debit Limit"
),
), ),
] ]

15
cashonly/core/migrations/0003_debit_limit_null_default.py

@ -8,13 +8,20 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('cashonly_core', '0002_account_debit_limit'), ("cashonly_core", "0002_account_debit_limit"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='account', model_name="account",
name='debit_limit', name="debit_limit",
field=models.DecimalField(blank=True, decimal_places=2, default=None, max_digits=5, null=True, verbose_name='debit limit'), field=models.DecimalField(
blank=True,
decimal_places=2,
default=None,
max_digits=5,
null=True,
verbose_name="debit limit",
),
), ),
] ]

10
cashonly/core/migrations/0004_account_avatar.py

@ -8,13 +8,15 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('cashonly_core', '0003_debit_limit_null_default'), ("cashonly_core", "0003_debit_limit_null_default"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='account', model_name="account",
name='avatar', name="avatar",
field=models.ImageField(blank=True, null=True, upload_to='avatars', verbose_name='avatar'), field=models.ImageField(
blank=True, null=True, upload_to="avatars", verbose_name="avatar"
),
), ),
] ]

103
cashonly/core/models.py

@ -1,38 +1,34 @@
from django.conf import settings
from django.db import models from django.db import models
from django.core.files import File
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import gettext_lazy as _
class Account(models.Model): class Account(models.Model):
user = models.OneToOneField( user = models.OneToOneField(User, on_delete=models.CASCADE)
User
)
card_number = models.CharField( card_number = models.CharField(
verbose_name=_('card number'), verbose_name=_("card number"),
max_length=32, max_length=32,
unique=True, unique=True,
blank=True, blank=True,
null=True, null=True,
) )
pin = models.CharField( pin = models.CharField(
verbose_name=_('PIN'), verbose_name=_("PIN"),
max_length=32, max_length=32,
blank=True, blank=True,
) )
daily_digest = models.BooleanField( daily_digest = models.BooleanField(
verbose_name=_('daily digest'), verbose_name=_("daily digest"),
default=True, default=True,
) )
credit = models.DecimalField( credit = models.DecimalField(
verbose_name=_('credit'), verbose_name=_("credit"),
max_digits=5, max_digits=5,
decimal_places=2, decimal_places=2,
default=0, default=0,
) )
debit_limit = models.DecimalField( debit_limit = models.DecimalField(
verbose_name=_('debit limit'), verbose_name=_("debit limit"),
max_digits=5, max_digits=5,
decimal_places=2, decimal_places=2,
default=None, default=None,
@ -40,8 +36,8 @@ class Account(models.Model):
null=True, null=True,
) )
avatar = models.ImageField( avatar = models.ImageField(
verbose_name=_('avatar'), verbose_name=_("avatar"),
upload_to='avatars', upload_to="avatars",
blank=True, blank=True,
null=True, null=True,
) )
@ -50,60 +46,61 @@ class Account(models.Model):
return self.user.username return self.user.username
class Meta: class Meta:
verbose_name = _('account') verbose_name = _("account")
verbose_name_plural = _('accounts') verbose_name_plural = _("accounts")
class ProductCategory(models.Model): class ProductCategory(models.Model):
name = models.CharField( name = models.CharField(
verbose_name=_('name'), verbose_name=_("name"),
max_length=32, max_length=32,
unique=True, unique=True,
) )
comment = models.CharField( comment = models.CharField(
verbose_name=_('comment'), verbose_name=_("comment"),
max_length=128, max_length=128,
blank=True, blank=True,
) )
def __str__(self): def __str__(self):
return '%s (%s)' % (self.name, self.comment) return "%s (%s)" % (self.name, self.comment)
class Meta: class Meta:
verbose_name = _('product category') verbose_name = _("product category")
verbose_name_plural = _('product categories') verbose_name_plural = _("product categories")
class Product(models.Model): class Product(models.Model):
name = models.CharField( name = models.CharField(
verbose_name=_('name'), verbose_name=_("name"),
max_length=32, max_length=32,
unique=True, unique=True,
) )
price = models.DecimalField( price = models.DecimalField(
verbose_name=_('price'), verbose_name=_("price"),
max_digits=5, max_digits=5,
decimal_places=2, decimal_places=2,
) )
active = models.BooleanField( active = models.BooleanField(
verbose_name=_('active'), verbose_name=_("active"),
default=True, default=True,
) )
category = models.ForeignKey( category = models.ForeignKey(
ProductCategory, ProductCategory,
verbose_name=_('category'), on_delete=models.CASCADE,
verbose_name=_("category"),
blank=True, blank=True,
null=True, null=True,
) )
image = models.ImageField( image = models.ImageField(
verbose_name=_('image'), verbose_name=_("image"),
upload_to='products', upload_to="products",
blank=True, blank=True,
null=True, null=True,
) )
image_thumbnail = models.ImageField( image_thumbnail = models.ImageField(
verbose_name=_('image'), verbose_name=_("image"),
upload_to='products_thumb', upload_to="products_thumb",
blank=True, blank=True,
null=True, null=True,
) )
@ -112,88 +109,90 @@ class Product(models.Model):
return self.name return self.name
class Meta: class Meta:
verbose_name = _('product') verbose_name = _("product")
verbose_name_plural = _('products') verbose_name_plural = _("products")
class ProductBarcode(models.Model): class ProductBarcode(models.Model):
barcode = models.CharField( barcode = models.CharField(
verbose_name=_('barcode'), verbose_name=_("barcode"),
max_length=32, max_length=32,
unique=True, unique=True,
) )
comment = models.CharField( comment = models.CharField(
verbose_name=_('comment'), verbose_name=_("comment"),
max_length=128, max_length=128,
blank=True, blank=True,
) )
product = models.ForeignKey( product = models.ForeignKey(
Product, Product,
verbose_name=_('product'), on_delete=models.CASCADE,
verbose_name=_("product"),
) )
def __unicode__(self): def __unicode__(self):
return self.barcode return self.barcode
class Meta: class Meta:
verbose_name = _('barcode') verbose_name = _("barcode")
verbose_name_plural = _('barcodes') verbose_name_plural = _("barcodes")
class Transaction(models.Model): class Transaction(models.Model):
account = models.ForeignKey( account = models.ForeignKey(
Account, Account,
verbose_name=_('account'), on_delete=models.CASCADE,
verbose_name=_("account"),
) )
timestamp = models.DateTimeField( timestamp = models.DateTimeField(
verbose_name=_('timestamp'), verbose_name=_("timestamp"),
auto_now_add=True, auto_now_add=True,
) )
subject = models.CharField( subject = models.CharField(
verbose_name=_('subject'), verbose_name=_("subject"),
max_length=32, max_length=32,
) )
description = models.TextField( description = models.TextField(
verbose_name=_('description'), verbose_name=_("description"),
) )
amount = models.DecimalField( amount = models.DecimalField(
verbose_name=_('amount'), verbose_name=_("amount"),
max_digits=5, max_digits=5,
decimal_places=2, decimal_places=2,
) )
class Meta: class Meta:
verbose_name = _('transaction') verbose_name = _("transaction")
verbose_name_plural = _('transactions') verbose_name_plural = _("transactions")
class SalesLogEntry(models.Model): class SalesLogEntry(models.Model):
account = models.ForeignKey( account = models.ForeignKey(
Account, Account,
verbose_name=_('account'), on_delete=models.CASCADE,
verbose_name=_("account"),
) )
product = models.ForeignKey( product = models.ForeignKey(
Product, Product,
verbose_name=_('product'), on_delete=models.CASCADE,
verbose_name=_("product"),
) )
count = models.IntegerField( count = models.IntegerField(
verbose_name=_('count'), verbose_name=_("count"),
) )
unit_price = models.DecimalField( unit_price = models.DecimalField(
verbose_name=_('unit price'), verbose_name=_("unit price"),
max_digits=5, max_digits=5,
decimal_places=2, decimal_places=2,
) )
timestamp = models.DateTimeField( timestamp = models.DateTimeField(
verbose_name=_('timestamp'), verbose_name=_("timestamp"),
auto_now_add=True, auto_now_add=True,
) )
def __unicode__(self): def __unicode__(self):
return '%dx %s - %s' % (self.count, self.product, self.account) return "%dx %s - %s" % (self.count, self.product, self.account)
class Meta: class Meta:
verbose_name = _('sales log entry') verbose_name = _("sales log entry")
verbose_name_plural = _('sales log entries') verbose_name_plural = _("sales log entries")

53
cashonly/core/services.py

@ -1,7 +1,7 @@
from cashonly.core.models import Account, Transaction, SalesLogEntry, Product from cashonly.core.models import Account, Transaction, SalesLogEntry, Product
from django.core.files import File from django.core.files import File
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.translation import ugettext_noop from django.utils.translation import gettext_noop
from django.db.models.signals import pre_save, post_save, pre_delete from django.db.models.signals import pre_save, post_save, pre_delete
from django.dispatch import receiver from django.dispatch import receiver
from django.db import transaction from django.db import transaction
@ -20,51 +20,56 @@ class AccountManager:
self.account.credit = self.account.credit + amount self.account.credit = self.account.credit + amount
self.account.save() self.account.save()
Transaction.objects.create(account=self.account, subject=subject, Transaction.objects.create(
amount=amount, description=description) account=self.account,
subject=subject,
amount=amount,
description=description,
)
def change_credit(self, amount, operator, comment=None): def change_credit(self, amount, operator, comment=None):
if amount > 0: if amount > 0:
subject = ugettext_noop('Deposit') subject = gettext_noop("Deposit")
elif amount < 0: elif amount < 0:
subject = ugettext_noop('Payout') subject = gettext_noop("Payout")
else: else:
raise ValueError('Amount must not be zero.') raise ValueError("Amount must not be zero.")
desc = ugettext_noop('Authorized by %(first)s %(last)s') % \ desc = gettext_noop("Authorized by %(first)s %(last)s") % {
{'first': operator.first_name, 'last': operator.last_name} "first": operator.first_name,
"last": operator.last_name,
}
if comment is not None and len(comment) > 0: if comment is not None and len(comment) > 0:
desc += ' (%s)' % (comment) desc += " (%s)" % (comment)
self.add_transaction(amount, subject, desc) self.add_transaction(amount, subject, desc)
@transaction.atomic @transaction.atomic
def buy_products(self, products): def buy_products(self, products):
if min(products.values()) <= 0: if min(products.values()) <= 0:
raise ValueError('Non-positive amount in products dict.') raise ValueError("Non-positive amount in products dict.")
total_value = sum(map(lambda p: p.price * products[p], total_value = sum(map(lambda p: p.price * products[p], products.keys()))
products.keys()))
if self.account.debit_limit is not None: if self.account.debit_limit is not None:
debit_limit = self.account.debit_limit debit_limit = self.account.debit_limit
else: else:
debit_limit = settings.CASHONLY_DEFAULT_DEBIT_LIMIT debit_limit = settings.CASHONLY_DEFAULT_DEBIT_LIMIT
if self.account.credit - total_value >= debit_limit * -1: if self.account.credit - total_value >= debit_limit * -1:
desc = '' desc = ""
for product in products.keys(): for product in products.keys():
if not product.active: if not product.active:
raise ValueError('Trying to buy a disabled product.') raise ValueError("Trying to buy a disabled product.")
amount = products[product] amount = products[product]
desc += '%d x %s\n' % (amount, product.name) desc += "%d x %s\n" % (amount, product.name)
SalesLogEntry.objects.create( SalesLogEntry.objects.create(
account=self.account, account=self.account,
product=product, product=product,
count=amount, count=amount,
unit_price=product.price unit_price=product.price,
) )
self.add_transaction(-total_value, ugettext_noop('Purchase'), desc) self.add_transaction(-total_value, gettext_noop("Purchase"), desc)
return True return True
else: else:
return False return False
@ -77,7 +82,7 @@ class AccountManager:
self.account.save() self.account.save()
def clear_pin(self): def clear_pin(self):
self.set_pin('') self.set_pin("")
def check_pin(self, pin): def check_pin(self, pin):
return self.account.pin == pin return self.account.pin == pin
@ -95,8 +100,8 @@ def logentry_pre_delete_handler(sender, instance, **kwargs):
accmgr = AccountManager(instance.account) accmgr = AccountManager(instance.account)
accmgr.add_transaction( accmgr.add_transaction(
instance.unit_price * instance.count, instance.unit_price * instance.count,
ugettext_noop('Cancellation'), gettext_noop("Cancellation"),
'%d x %s' % (instance.count, instance.product.name) "%d x %s" % (instance.count, instance.product.name),
) )
@ -106,14 +111,12 @@ def product_post_save_handler(sender, instance, **kwargs):
img = instance.image img = instance.image
if img: if img:
scaledFile = io.StringIO() scaledFile = io.StringIO()
img.open(mode='r') img.open(mode="r")
with img: with img:
scaled = PIL.Image.open(img) scaled = PIL.Image.open(img)
thumbnail_size = getattr(settings, 'THUMBNAIL_SIZE', (150, 150)) thumbnail_size = getattr(settings, "THUMBNAIL_SIZE", (150, 150))
scaled.thumbnail(thumbnail_size, PIL.Image.ANTIALIAS) scaled.thumbnail(thumbnail_size, PIL.Image.ANTIALIAS)
scaled.save(scaledFile, 'PNG') scaled.save(scaledFile, "PNG")
scaledFile.seek(0) scaledFile.seek(0)
instance.image_thumbnail.save(img.url, File(scaledFile), save=False) instance.image_thumbnail.save(img.url, File(scaledFile), save=False)

6
cashonly/web/__init__.py

@ -2,8 +2,8 @@ from django.apps import AppConfig
class CashonlyWebAppConfig(AppConfig): class CashonlyWebAppConfig(AppConfig):
name = 'cashonly.web' name = "cashonly.web"
label = 'cashonly_web' label = "cashonly_web"
default_app_config = 'cashonly.web.CashonlyWebAppConfig' default_app_config = "cashonly.web.CashonlyWebAppConfig"

2
cashonly/web/templates/cashonly/web/base.html

@ -1,4 +1,4 @@
{% load staticfiles %} {% load static %}
{% load i18n %} {% load i18n %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">

2
cashonly/web/templates/cashonly/web/login.html

@ -1,5 +1,5 @@
{% load i18n %} {% load i18n %}
{% load staticfiles %} {% load static %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">

2
cashonly/web/templates/cashonly/web/usersettings.html

@ -1,7 +1,7 @@
{% extends "cashonly/web/base.html" %} {% extends "cashonly/web/base.html" %}
{% load i18n %} {% load i18n %}
{% load bootstrap %} {% load bootstrap %}
{% load staticfiles %} {% load static %}
{% block head %} {% block head %}
{{ block.super }} {{ block.super }}

61
cashonly/web/urls.py

@ -1,33 +1,36 @@
from django.conf.urls import url from django.urls import re_path
from cashonly.web import views from cashonly.web import views
urlpatterns = [ urlpatterns = [
url(r'^$', views.overview, name='overview'), re_path(r"^$", views.overview, name="overview"),
re_path(r"^product/(?P<pk>\d+)/$", views.ProductView.as_view(), name="product"),
url(r'^product/(?P<pk>\d+)/$', views.ProductView.as_view(), re_path(
name='product'), r"transactions/$",
views.transactions,
url(r'transactions/$', views.transactions, {'detailed': False, 'page': 1}, {"detailed": False, "page": 1},
name='transactions'), name="transactions",
),
url(r'transactions/(?P<page>\d+)/$', views.transactions, re_path(
{'detailed': False}, name='transactions'), r"transactions/(?P<page>\d+)/$",
views.transactions,
url(r'transactions/(?P<page>\d+)/detailed/$', views.transactions, {"detailed": False},
{'detailed': True}, name='transactions_detailed'), name="transactions",
),
url(r'products/((?P<category_id>\d+)/)?$', views.products, re_path(
name='products'), r"transactions/(?P<page>\d+)/detailed/$",
views.transactions,
url(r'buy/(?P<product_id>\d+)/$', views.buy, name='buy'), {"detailed": True},
name="transactions_detailed",
url(r'buy/(?P<product_id>\d+)/really/$', views.buy, ),
{'confirm': True}, name='buy_really'), re_path(r"products/((?P<category_id>\d+)/)?$", views.products, name="products"),
re_path(r"buy/(?P<product_id>\d+)/$", views.buy, name="buy"),
url(r'buy/thanks/$', views.buy_thanks, name='buy_thanks'), re_path(
r"buy/(?P<product_id>\d+)/really/$",
url(r'buy/error/$', views.buy_error, name='buy_error'), views.buy,
{"confirm": True},
url(r'usersettings(/(?P<submit>\w+))?/$', views.usersettings, name="buy_really",
name='usersettings'), ),
re_path(r"buy/thanks/$", views.buy_thanks, name="buy_thanks"),
re_path(r"buy/error/$", views.buy_error, name="buy_error"),
re_path(r"usersettings(/(?P<submit>\w+))?/$", views.usersettings, name="usersettings"),
] ]

165
cashonly/web/views.py

@ -3,17 +3,17 @@ from django import forms
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core import paginator from django.core import paginator
from django.contrib.auth.models import User
from cashonly.core.models import * from cashonly.core.models import *
from cashonly.core.services import AccountManager from cashonly.core.services import AccountManager
from django.utils.translation import ugettext as _ from django.utils.translation import gettext as _
from django.utils.translation import ugettext_lazy from django.utils.translation import gettext_lazy
from django.db import IntegrityError from django.db import IntegrityError
#import cashonly.core.version
# import cashonly.core.version
import datetime import datetime
#def version_number_context_processor(request): # def version_number_context_processor(request):
# return {'version_number': cashonly.version.CASHONLY_VERSION} # return {'version_number': cashonly.version.CASHONLY_VERSION}
@ -22,17 +22,26 @@ def overview(request):
try: try:
a = request.user.account a = request.user.account
except User.account.RelatedObjectDoesNotExist: except User.account.RelatedObjectDoesNotExist:
return render(request, 'cashonly/web/index.html', return render(
{'latest_transactions': [], 'latest_purchases': []}) request,
"cashonly/web/index.html",
{"latest_transactions": [], "latest_purchases": []},
)
time = datetime.datetime.now() - datetime.timedelta(hours=12) time = datetime.datetime.now() - datetime.timedelta(hours=12)
transactions = Transaction.objects.filter(account=a) \ transactions = (
.filter(timestamp__gte=time).order_by('-timestamp') Transaction.objects.filter(account=a)
.filter(timestamp__gte=time)
.order_by("-timestamp")
)
# FIXME: distinct doesn't work as expected, so fetch 20 rows and hope that # FIXME: distinct doesn't work as expected, so fetch 20 rows and hope that
# there are 3 distinct products # there are 3 distinct products
purchases = Product.objects.filter(saleslogentry__account=a) \ purchases = (
.order_by('-saleslogentry__timestamp').distinct()[:20] Product.objects.filter(saleslogentry__account=a)
.order_by("-saleslogentry__timestamp")
.distinct()[:20]
)
products = [] products = []
# Find 3 products # Find 3 products
@ -43,9 +52,11 @@ def overview(request):
if len(products) == 3: if len(products) == 3:
break break
return render(request, 'cashonly/web/index.html', return render(
{'latest_transactions': transactions, request,
'latest_purchases': products}) "cashonly/web/index.html",
{"latest_transactions": transactions, "latest_purchases": products},
)
class ProductView(generic.DetailView): class ProductView(generic.DetailView):
@ -55,7 +66,7 @@ class ProductView(generic.DetailView):
@login_required @login_required
def transactions(request, detailed, page): def transactions(request, detailed, page):
a = request.user.account a = request.user.account
transactions = Transaction.objects.filter(account=a).order_by('-timestamp') transactions = Transaction.objects.filter(account=a).order_by("-timestamp")
if page is None: if page is None:
page = 1 page = 1
@ -66,9 +77,11 @@ def transactions(request, detailed, page):
except paginator.EmptyPage: except paginator.EmptyPage:
transaction_list = paginator.page(paginator.num_pages) transaction_list = paginator.page(paginator.num_pages)
return render(request, 'cashonly/web/transaction_list.html', return render(
{'transaction_list': transaction_list, request,
'detailed': detailed}) "cashonly/web/transaction_list.html",
{"transaction_list": transaction_list, "detailed": detailed},
)
def products(request, category_id=None): def products(request, category_id=None):
@ -77,14 +90,15 @@ def products(request, category_id=None):
products = Product.objects.filter(active=True) products = Product.objects.filter(active=True)
else: else:
category = get_object_or_404(ProductCategory, id=category_id) category = get_object_or_404(ProductCategory, id=category_id)
products = Product.objects.filter(active=True) \ products = Product.objects.filter(active=True).filter(category=category)
.filter(category=category)
categories = ProductCategory.objects.all() categories = ProductCategory.objects.all()
return render(request, 'cashonly/web/product_list.html', return render(
{'product_list': products, 'category': category, request,
'categories': categories}) "cashonly/web/product_list.html",
{"product_list": products, "category": category, "categories": categories},
)
@login_required @login_required
@ -94,121 +108,134 @@ def buy(request, product_id, confirm=False):
if confirm: if confirm:
accmgr = AccountManager(request.user.account) accmgr = AccountManager(request.user.account)
if accmgr.buy_product(product, 1): if accmgr.buy_product(product, 1):
return redirect('buy_thanks') return redirect("buy_thanks")
else: else:
return redirect('buy_error') return redirect("buy_error")
else: else:
return render(request, 'cashonly/web/buy_confirm.html', return render(request, "cashonly/web/buy_confirm.html", {"product": product})
{'product': product})
@login_required @login_required
def buy_thanks(request): def buy_thanks(request):
return render(request, 'cashonly/web/buy_thanks.html') return render(request, "cashonly/web/buy_thanks.html")
@login_required @login_required
def buy_error(request): def buy_error(request):
return render(request, 'cashonly/web/buy_error.html') return render(request, "cashonly/web/buy_error.html")
class UserSettingsForm(forms.Form): class UserSettingsForm(forms.Form):
daily_digest = forms.BooleanField(required=False, daily_digest = forms.BooleanField(
label=ugettext_lazy('daily digest')) required=False, label=gettext_lazy("daily digest")
)
class UserPinForm(forms.Form): class UserPinForm(forms.Form):
pin = forms.CharField(max_length=32, widget=forms.PasswordInput, pin = forms.CharField(
label=ugettext_lazy('PIN'), required=False) max_length=32,
pin_confirm = forms.CharField(max_length=32, widget=forms.PasswordInput, widget=forms.PasswordInput,
label=ugettext_lazy('PIN (confirmation)'), label=gettext_lazy("PIN"),
required=False) required=False,
)
pin_confirm = forms.CharField(
max_length=32,
widget=forms.PasswordInput,
label=gettext_lazy("PIN (confirmation)"),
required=False,
)
def clean(self): def clean(self):
cleaned_data = super(UserPinForm, self).clean() cleaned_data = super(UserPinForm, self).clean()
if 'pin' not in cleaned_data and 'pin_confirm' not in cleaned_data: if "pin" not in cleaned_data and "pin_confirm" not in cleaned_data:
return cleaned_data return cleaned_data
if cleaned_data['pin'] != cleaned_data['pin_confirm']: if cleaned_data["pin"] != cleaned_data["pin_confirm"]:
raise forms.ValidationError(_('PINs do not match.')) raise forms.ValidationError(_("PINs do not match."))
return cleaned_data return cleaned_data
class UserAvatarForm(forms.Form): class UserAvatarForm(forms.Form):
avatar = forms.ImageField(label=ugettext_lazy('avatar'), required=False) avatar = forms.ImageField(label=gettext_lazy("avatar"), required=False)
class UserCardNumberForm(forms.Form): class UserCardNumberForm(forms.Form):
card_number = forms.CharField(max_length=32, card_number = forms.CharField(
label=ugettext_lazy('card number'), max_length=32, label=gettext_lazy("card number"), required=False
required=False) )
@login_required @login_required
def usersettings(request, submit=None): def usersettings(request, submit=None):
daily_digest = request.user.account.daily_digest daily_digest = request.user.account.daily_digest
settings_form = UserSettingsForm({'daily_digest': daily_digest}) settings_form = UserSettingsForm({"daily_digest": daily_digest})
pin_form = UserPinForm() pin_form = UserPinForm()
avatar_form = UserAvatarForm() avatar_form = UserAvatarForm()
card_number_form = UserCardNumberForm({ card_number_form = UserCardNumberForm(
'card_number': request.user.account.card_number {"card_number": request.user.account.card_number}
}) )
if request.method == 'POST': if request.method == "POST":
if submit == 'pin': if submit == "pin":
pin_form = UserPinForm(request.POST) pin_form = UserPinForm(request.POST)
if pin_form.is_valid(): if pin_form.is_valid():
pin = pin_form.cleaned_data['pin'] pin = pin_form.cleaned_data["pin"]
accmgr = AccountManager(request.user.account) accmgr = AccountManager(request.user.account)
if pin is not None: if pin is not None:
accmgr.set_pin(pin) accmgr.set_pin(pin)
else: else:
accmgr.clear_pin() accmgr.clear_pin()
return render(request, 'cashonly/web/usersettings_saved.html') return render(request, "cashonly/web/usersettings_saved.html")
elif submit == 'settings': elif submit == "settings":
settings_form = UserSettingsForm(request.POST) settings_form = UserSettingsForm(request.POST)
if settings_form.is_valid(): if settings_form.is_valid():
daily_digest = settings_form.cleaned_data['daily_digest'] daily_digest = settings_form.cleaned_data["daily_digest"]
request.user.account.daily_digest = daily_digest request.user.account.daily_digest = daily_digest
request.user.account.save() request.user.account.save()
return render(request, 'cashonly/web/usersettings_saved.html') return render(request, "cashonly/web/usersettings_saved.html")
elif submit == 'avatar': elif submit == "avatar":
avatar_form = UserAvatarForm(request.POST, request.FILES) avatar_form = UserAvatarForm(request.POST, request.FILES)
if avatar_form.is_valid(): if avatar_form.is_valid():
if 'delete' in request.POST: if "delete" in request.POST:
request.user.account.avatar = None request.user.account.avatar = None
else: else:
request.user.account.avatar = \ request.user.account.avatar = avatar_form.cleaned_data["avatar"]
avatar_form.cleaned_data['avatar']
request.user.account.save() request.user.account.save()
return render(request, 'cashonly/web/usersettings_saved.html') return render(request, "cashonly/web/usersettings_saved.html")
else: else:
# TODO handle upload error (e.g. wrong mime type?) # TODO handle upload error (e.g. wrong mime type?)
pass pass
elif submit == 'card_number': elif submit == "card_number":
card_number_form = UserCardNumberForm(request.POST) card_number_form = UserCardNumberForm(request.POST)
# TODO validate card number # TODO validate card number
if card_number_form.is_valid(): if card_number_form.is_valid():
request.user.account.card_number = \ request.user.account.card_number = card_number_form.cleaned_data[
card_number_form.cleaned_data['card_number'] "card_number"
]
try: try:
request.user.account.save() request.user.account.save()
except IntegrityError: except IntegrityError:
return render( return render(
request, request,
'cashonly/web/usersettings_error.html', "cashonly/web/usersettings_error.html",
{'msg': _('Card number is already in use.')} {"msg": _("Card number is already in use.")},
) )
return render(request, 'cashonly/web/usersettings_saved.html') return render(request, "cashonly/web/usersettings_saved.html")
else: else:
pass pass
# TODO handle error (invalid card number) # TODO handle error (invalid card number)
return render(request, 'cashonly/web/usersettings.html', return render(
{'settings_form': settings_form, 'pin_form': pin_form, request,
'avatar_form': avatar_form, "cashonly/web/usersettings.html",
'card_number_form': card_number_form}) {
"settings_form": settings_form,
"pin_form": pin_form,
"avatar_form": avatar_form,
"card_number_form": card_number_form,
},
)

4
config/copysecret.py

@ -6,7 +6,7 @@ for line in fp:
break break
fp.close() fp.close()
with open('/django/config/settings.py', 'r') as file: with open("/django/config/settings.py", "r") as file:
data = file.readlines() data = file.readlines()
for i, line in enumerate(data): for i, line in enumerate(data):
@ -14,5 +14,5 @@ for i, line in enumerate(data):
data[i] = "{} = {}".format("SECRET_KEY", key) data[i] = "{} = {}".format("SECRET_KEY", key)
break break
with open('/django/config/settings.py', 'w') as file: with open("/django/config/settings.py", "w") as file:
file.writelines(data) file.writelines(data)

92
config/settings.py

@ -16,68 +16,68 @@ import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '&h!kbrmk)la2m%+%-qmdj1&ifg^rj3$o*)sos2dedw$$b4_lp$' SECRET_KEY = "&h!kbrmk)la2m%+%-qmdj1&ifg^rj3$o*)sos2dedw$$b4_lp$"
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
ALLOWED_HOSTS = ['*'] ALLOWED_HOSTS = ["*"]
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'django.contrib.admin', "django.contrib.admin",
'django.contrib.auth', "django.contrib.auth",
'django.contrib.contenttypes', "django.contrib.contenttypes",
'django.contrib.sessions', "django.contrib.sessions",
'django.contrib.messages', "django.contrib.messages",
'django.contrib.staticfiles', "django.contrib.staticfiles",
'cashonly.core', "cashonly.core",
'cashonly.web', "cashonly.web",
'bootstrapform', "bootstrapform",
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', "django.middleware.security.SecurityMiddleware",
'django.contrib.sessions.middleware.SessionMiddleware', "django.contrib.sessions.middleware.SessionMiddleware",
'django.middleware.common.CommonMiddleware', "django.middleware.common.CommonMiddleware",
'django.middleware.csrf.CsrfViewMiddleware', "django.middleware.csrf.CsrfViewMiddleware",
'django.contrib.auth.middleware.AuthenticationMiddleware', "django.contrib.auth.middleware.AuthenticationMiddleware",
'django.contrib.messages.middleware.MessageMiddleware', "django.contrib.messages.middleware.MessageMiddleware",
'django.middleware.clickjacking.XFrameOptionsMiddleware', "django.middleware.clickjacking.XFrameOptionsMiddleware",
] ]
ROOT_URLCONF = 'server.urls' ROOT_URLCONF = "server.urls"
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', "BACKEND": "django.template.backends.django.DjangoTemplates",
'DIRS': [], "DIRS": [],
'APP_DIRS': True, "APP_DIRS": True,
'OPTIONS': { "OPTIONS": {
'context_processors': [ "context_processors": [
'django.template.context_processors.debug', "django.template.context_processors.debug",
'django.template.context_processors.request', "django.template.context_processors.request",
'django.contrib.auth.context_processors.auth', "django.contrib.auth.context_processors.auth",
'django.contrib.messages.context_processors.messages', "django.contrib.messages.context_processors.messages",
], ],
}, },
}, },
] ]
WSGI_APPLICATION = 'server.wsgi.application' WSGI_APPLICATION = "server.wsgi.application"
# Database # Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases # https://docs.djangoproject.com/en/1.11/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { "default": {
'ENGINE': 'django.db.backends.postgresql', "ENGINE": "django.db.backends.postgresql",
'NAME': 'postgres', "NAME": "postgres",
'USER': 'postgres', "USER": "postgres",
'PASSWORD': os.environ.get("POSTGRES_PASSWORD"), "PASSWORD": os.environ.get("POSTGRES_PASSWORD"),
'HOST': os.environ.get("POSTGRES_HOST"), "HOST": os.environ.get("POSTGRES_HOST"),
'PORT': 5432, # default "PORT": 5432, # default
} }
} }
@ -86,27 +86,27 @@ DATABASES = {
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ {
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
}, },
] ]
LOGIN_URL = '/login/' LOGIN_URL = "/login/"
LOGIN_REDIRECT_URL = '/' LOGIN_REDIRECT_URL = "/"
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/ # https://docs.djangoproject.com/en/1.11/topics/i18n/
LANGUAGE_CODE = 'de-de' LANGUAGE_CODE = "de-de"
TIME_ZONE = 'UTC' TIME_ZONE = "UTC"
USE_I18N = True USE_I18N = True
USE_L10N = True USE_L10N = True
USE_TZ = True USE_TZ = True
@ -114,8 +114,8 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/ # https://docs.djangoproject.com/en/1.11/howto/static-files/
STATIC_URL = '/static/' STATIC_URL = "/static/"
STATICFILES_DIRS = ['/django/static_thirdparty'] STATICFILES_DIRS = ["/django/static_thirdparty"]
# Cashonly Stuff # Cashonly Stuff
# https://git.blinkenbunt.org/cashonly/cashonly # https://git.blinkenbunt.org/cashonly/cashonly

14
config/urls.py

@ -13,15 +13,19 @@ Including another URLconf
1. Import the include() function: from django.conf.urls import url, include 1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
""" """
from django.conf.urls import url, include from django.urls import re_path, include
from django.contrib import admin from django.contrib import admin
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from django.conf.urls.static import static from django.conf.urls.static import static
from django.conf import settings from django.conf import settings
urlpatterns = [ urlpatterns = [
url(r'^admin/', admin.site.urls), re_path(r"^admin/", admin.site.urls),
url(r'', include('cashonly.web.urls')), re_path(r"", include("cashonly.web.urls")),
url(r'^login/$', auth_views.LoginView.as_view(template_name='cashonly/web/login.html'), name='login'), re_path(
url(r'^logout/$', auth_views.logout_then_login, name='logout_then_login'), r"^login/$",
auth_views.LoginView.as_view(template_name="cashonly/web/login.html"),
name="login",
),
re_path(r"^logout/$", auth_views.logout_then_login, name="logout_then_login"),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

2
requirements.txt

@ -1,4 +1,4 @@
Django==1.11.20 Django==4.1.3
django-bootstrap-form==3.4 django-bootstrap-form==3.4
psycopg2-binary psycopg2-binary
Pillow Pillow

Loading…
Cancel
Save