Initial commit - existing project files

This commit is contained in:
W13R 2022-03-16 12:11:30 +01:00
commit c49798a9ea
82 changed files with 4304 additions and 0 deletions

View file

138
application/app/admin.py Normal file
View file

@ -0,0 +1,138 @@
#
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.views.decorators.cache import never_cache
from .models import User
from .models import Drink
from .models import Order
from .models import Global
from .models import RegisterTransaction as Register
from .forms import CustomUserChangeForm
from .forms import CustomDrinkForm
from .forms import CustomGlobalForm
from .forms import CustomRegisterTransactionForm
# Admin Site
class CustomAdminSite(admin.AdminSite):
site_header = "Drinks Administration"
site_title = "Drinks Administration"
@never_cache
def index(self, request, extra_context=None):
return super().index(request, extra_context={
"registerBalance": "{:10.2f}".format(
Global.objects.get(name="register_balance").value_float
),
"admin_info": Global.objects.get(name="admin_info").value_string,
**(extra_context or {})
})
adminSite = CustomAdminSite()
# Register your models here.
class CustomUserAdmin(UserAdmin):
model = User
form = CustomUserChangeForm
fieldsets_ = list((*UserAdmin.fieldsets,))
fieldsets_.insert(1, (
"Balance",
{"fields": ("balance", "allow_order_with_negative_balance")},
)
)
fieldsets = tuple(fieldsets_)
list_display = ["username", "balance", "is_active", "allow_order_with_negative_balance"]
def get_actions(self, request): # remove the "delete_selected" action because it breaks some functionality
actions = super().get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected']
return actions
adminSite.register(User, CustomUserAdmin)
class CustomDrinkAdmin(admin.ModelAdmin):
model = Drink
form = CustomDrinkForm
list_display = ["product_name", "content_litres", "price", "available", "binary_availability", "deleted"]
adminSite.register(Drink, CustomDrinkAdmin)
class CustomRegisterAdmin(admin.ModelAdmin):
model = Register
form = CustomRegisterTransactionForm
site_title = "Register"
list_display = ["datetime", "transaction_sum", "user", "comment"]
actions = ["delete_selected_new"]
def get_actions(self, request): # remove the "delete_selected" action because it breaks some functionality
actions = super().get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected']
return actions
def delete_selected_new(self, request, queryset):
#print(queryset)
for supply in queryset:
#print(order)
supply.delete()
if queryset.count() < 2:
self.message_user(request, f"Revoked {queryset.count()} supply.")
else:
self.message_user(request, f"Revoked {queryset.count()} supplies.")
delete_selected_new.short_description = "Revoke selected transactions"
adminSite.register(Register, CustomRegisterAdmin)
class CustomOrderAdmin(admin.ModelAdmin):
model = Order
list_display = ["product_name", "amount", "price_sum", "user", "datetime"]
actions = ["delete_selected_new"]
def get_actions(self, request): # remove the "delete_selected" action because it breaks some functionality
actions = super().get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected']
return actions
def delete_selected_new(self, request, queryset):
#print(queryset)
for order in queryset:
#print(order)
order.delete()
self.message_user(request, f"Revoked {queryset.count()} order(s).")
delete_selected_new.short_description = "Revoke selected orders"
adminSite.register(Order, CustomOrderAdmin)
class CustomGlobalAdmin(admin.ModelAdmin):
model = Global
form = CustomGlobalForm
list_display = ["name", "value_float", "value_string"]
def get_actions(self, request): # remove the "delete_selected" action because it breaks some functionality
actions = super().get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected']
return actions
adminSite.register(Global, CustomGlobalAdmin)

7
application/app/apps.py Normal file
View file

@ -0,0 +1,7 @@
from django.apps import AppConfig
from django.contrib.admin.apps import AdminConfig
class DAppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'app'

View file

@ -0,0 +1,16 @@
from django.conf import settings
from .models import Global
def app_version(request):
try:
global_message = Global.objects.get(pk="global_message").value_string
except Global.DoesNotExist:
global_message = ""
return {
"app_version": settings.APP_VERSION,
"currency_suffix": settings.CURRENCY_SUFFIX,
"global_message": global_message
}

47
application/app/forms.py Normal file
View file

@ -0,0 +1,47 @@
from django import forms
from django.conf import settings
from django.contrib.auth.forms import UserChangeForm
from .models import User
from .models import Drink
from .models import RegisterTransaction
from .models import Global
class CustomUserChangeForm(UserChangeForm):
balance = forms.DecimalField(max_digits=8, decimal_places=2, initial=0.00, label=f"Balance {settings.CURRENCY_SUFFIX}")
class Meta:
model = User
fields = ("username", "balance")
class CustomDrinkForm(forms.ModelForm):
product_name = forms.CharField(max_length=64, label="Product Name")
content_litres = forms.DecimalField(max_digits=6, decimal_places=3, initial=0.5, label="Content (l)")
price = forms.DecimalField(max_digits=6, decimal_places=2, label=f"Price {settings.CURRENCY_SUFFIX}")
class Meta:
model = Drink
fields = ("product_name", "content_litres", "price", "binary_availability", "available", "deleted")
class CustomRegisterTransactionForm(forms.ModelForm):
class Meta:
model = RegisterTransaction
fields = ("transaction_sum", "datetime", "is_user_deposit", "comment", "user")
class CustomGlobalForm(forms.ModelForm):
comment = forms.CharField(widget=forms.Textarea, required=False)
value_float = forms.FloatField(initial=0.00)
value_string = forms.CharField(widget=forms.Textarea, required=False)
class Meta:
model = Global
fields = ("name", "comment", "value_float", "value_string")

160
application/app/models.py Normal file
View file

@ -0,0 +1,160 @@
from django.db import models
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django_currentuser.db.models import CurrentUserField
from django.forms import ValidationError
from django.utils import timezone
# helper
def make_register_transaction(transaction_sum:float):
regbalance = Global.objects.get(name="register_balance")
regbalance.value_float += float(round(float(transaction_sum), 2))
regbalance.save()
# Custom user model
class User(AbstractUser):
balance = models.DecimalField(max_digits=8, decimal_places=2, default=0.00)
allow_order_with_negative_balance = models.BooleanField(default=False)
def delete(self, *args, **kwargs):
self.balance = 0
self.is_active = False
self.username = f"<deleted user #{self.pk}>"
self.last_name = ""
self.first_name = ""
self.email = ""
super().save()
#
class Drink(models.Model):
product_name = models.CharField(max_length=64)
content_litres = models.DecimalField(max_digits=6, decimal_places=3, default=0.5)
price = models.DecimalField(max_digits=6, decimal_places=2, default=0.00)
available = models.PositiveIntegerField(default=0)
deleted = models.BooleanField(default=False)
# when the following field is true:
# available > 0 -> there is a indefinetly amount of drinks left
# available < 1 -> there are no drinks left
binary_availability = models.BooleanField(default=False)
def delete(self, *args, **kwargs):
self.deleted = True
super().save()
def __str__(self): return f"{self.product_name} ({str(self.content_litres).rstrip('0')}l) - {self.price}{settings.CURRENCY_SUFFIX}"
class RegisterTransaction(models.Model):
class Meta:
verbose_name = "register transaction"
verbose_name_plural = "register"
transaction_sum = models.DecimalField(max_digits=6, decimal_places=2, default=0.00)
# the following original_transaction_sum is needed when need to be
# updated, but the old value needs to be known (field is hidden)
old_transaction_sum = models.DecimalField(max_digits=6, decimal_places=2, default=0.00)
datetime = models.DateTimeField(default=timezone.now)
is_user_deposit = models.BooleanField(default=False)
comment = models.TextField(default=" ")
user = CurrentUserField()
def save(self, *args, **kwargs):
if self._state.adding:
make_register_transaction(self.transaction_sum)
if self.is_user_deposit == True: # update user balance
self.user.balance += self.transaction_sum
self.user.save()
self.old_transaction_sum = self.transaction_sum
super().save(*args, **kwargs)
else:
# update register transaction
sum_diff = self.transaction_sum - self.old_transaction_sum
make_register_transaction(sum_diff)
# update user balance
if self.is_user_deposit == True:
ub_sum_diff = self.transaction_sum - self.old_transaction_sum
self.user.balance += ub_sum_diff
self.user.save()
self.old_transaction_sum = self.transaction_sum
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
make_register_transaction(-self.transaction_sum)
# update user deposit
if self.is_user_deposit:
self.user.balance -= self.transaction_sum
self.user.save()
super().delete(*args, kwargs)
def __str__(self): return f"{self.transaction_sum}{settings.CURRENCY_SUFFIX} by {self.user}"
class Order(models.Model):
drink = models.ForeignKey(
"Drink",
on_delete=models.SET_NULL,
null=True,
limit_choices_to=models.Q(available__gt=0) # Query only those drinks with a availability greater than (gt) 0
)
user = CurrentUserField()
datetime = models.DateTimeField(default=timezone.now)
amount = models.PositiveIntegerField(default=1, editable=False)
# the following fields will be set automatically
# won't use foreign key, because the values of the foreign objects may change over time.
product_name = models.CharField(max_length=64, editable=False)
price_sum = models.DecimalField(max_digits=6, decimal_places=2, default=0, editable=False)
content_litres = models.DecimalField(max_digits=6, decimal_places=3, default=0, editable=False)
# TODO: Add more comments on how and why the save & delete functions are implemented
# address this in a refactoring issue.
def save(self, *args, **kwargs):
drink = Drink.objects.get(pk=self.drink.pk)
if self._state.adding and drink.available > 0:
if not drink.binary_availability:
drink.available -= self.amount
drink.save()
self.product_name = drink.product_name
self.price_sum = drink.price * self.amount
self.content_litres = drink.content_litres
self.user.balance -= self.price_sum
self.user.save()
super().save(*args, **kwargs)
else:
raise ValidationError("This entry can't be changed.")
def delete(self, *args, **kwargs):
self.user.balance += self.price_sum
self.user.save()
drink = Drink.objects.get(pk=self.drink.pk)
if not drink.binary_availability:
drink.available += self.amount
drink.save()
super().delete(*args, **kwargs)
def __str__(self): return f"{self.drink.product_name} ({str(self.drink.content_litres).rstrip('0')}l) x {self.amount} - {self.price_sum}{settings.CURRENCY_SUFFIX}"
class Global(models.Model):
# this contains global values that are generated/calculated by code
# e.g. the current balance of the register, ...
name = models.CharField(max_length=42, unique=True, primary_key=True)
comment = models.TextField()
value_float = models.FloatField(default=0.00)
value_string = models.TextField()
def __str__(self): return self.name

View file

@ -0,0 +1,137 @@
#from datetime import datetime
from django.conf import settings
from django.db import connection
def _select_from_db(sql_select:str):
result = None
with connection.cursor() as cursor:
cursor.execute(sql_select)
result = cursor.fetchall()
return result
def select_history(user, language_code="en") -> list:
# select order history and deposits
user_id = user.pk
result = _select_from_db(f"""
select
concat(
product_name, ' (',
content_litres::real, -- converting to real removes trailing zeros
'l) x ', amount, ' - ', price_sum, '{settings.CURRENCY_SUFFIX}') as "text",
datetime
from app_order
where user_id = {user_id}
union
select
concat('Deposit: +', transaction_sum, '{settings.CURRENCY_SUFFIX}') as "text",
datetime
from app_userdeposits_view
where user_id = {user_id}
order by datetime desc
fetch first 30 rows only;
""")
result = [list(row) for row in result]
if language_code == "de": # reformat for german translation
for row in result:
row[0] = row[0].replace(".", ",")
return result
def select_yopml12m(user) -> list:
# number of orders per month (last 12 months)
# only for the specified user
user_id = user.pk
result = _select_from_db(f"""
-- select the count of the orders per month (last 12 days)
select
to_char(date_trunc('month', datetime), 'YYYY-MM') as "month",
sum(amount) as "count"
from app_order
where user_id = {user_id}
and date_trunc('month', datetime) > date_trunc('month', now() - '12 months'::interval)
group by "month"
order by "month" desc;
""")
return [list(row) for row in result]
def select_aopml12m() -> list:
# number of orders per month (last 12 months)
result = _select_from_db(f"""
-- select the count of the orders per month (last 12 days)
select
to_char(date_trunc('month', datetime), 'YYYY-MM') as "month",
sum(amount) as "count"
from app_order
where date_trunc('month', datetime) > date_trunc('month', now() - '12 months'::interval)
group by "month"
order by "month" desc;
""")
return [list(row) for row in result]
def select_yopwd(user) -> list:
# number of orders per weekday (all time)
# only for the specified user
user_id = user.pk
result = _select_from_db(f"""
-- select the count of the orders per weekday (all time)
select
to_char(datetime, 'Day') as "day",
sum(amount) as "count"
from app_order
where user_id = {user_id}
group by "day"
order by "count" desc;
""")
return [list(row) for row in result]
return []
def select_aopwd() -> list:
# number of orders per weekday (all time)
result = _select_from_db(f"""
-- select the count of the orders per weekday (all time)
select
to_char(datetime, 'Day') as "day",
sum(amount) as "count"
from app_order
group by "day"
order by "count" desc;
""")
return [list(row) for row in result]
return []
def select_noyopd(user) -> list:
# number of orders per drink (all time)
# only for specified user
user_id = user.pk
result = _select_from_db(f"""
select
d.product_name as "label",
sum(o.amount) as "data"
from app_drink d
join app_order o on (d.id = o.drink_id)
where o.user_id = {user_id}
group by d.product_name
order by "data" desc;
""")
return [list(row) for row in result]
def select_noaopd() -> list:
# number of orders per drink (all time)
result = _select_from_db(f"""
select
d.product_name as "label",
sum(o.amount) as "data"
from app_drink d
join app_order o on (d.id = o.drink_id)
group by d.product_name
order by "data" desc;
""")
return [list(row) for row in result]

View file

@ -0,0 +1,18 @@
{% extends "admin/base.html" %}
{% block title %}
{% if subtitle %}
{{ subtitle }} |
{% endif %}
{{ title }} | {{ site_title|default:_('Django site admin') }}
{% endblock %}
{% block extrahead %}
<link rel="shortcut icon" href="/static/favicon.png" sizes="480x480" />
{% endblock %}
{% block branding %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">{{ site_header|default:_('Django administration') }}</a></h1>
{% endblock %}
{% block nav-global %}{% endblock %}

View file

@ -0,0 +1,19 @@
{% extends "admin/index.html" %}
{% block sidebar %}
{{ block.super }}
<div>
<div>
<p>Current Register Balance: {{ registerBalance }}{{ currency_suffix }}</p>
{% if global_message != "" %}
<p>Global Message: {{ global_message }}</p>
{% endif %}
{% if admin_info != "" %}
<p>Admin Info: {{ admin_info }}</p>
{% endif %}
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,59 @@
<!DOCTYPE html>
{% load i18n %}
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=0.95">
<link rel="stylesheet" href="/static/css/main.css">
<link rel="shortcut icon" href="/static/favicon.png" sizes="480x480" />
<title>{% block title %}{% endblock %}</title>
{% block headAdditional %}{% endblock %}
</head>
<body>
<div class="baseLayout">
{% include "globalMessage.html" %}
{% if user.is_authenticated %}
<div class="userPanel">
{% include "userPanel.html" %}
</div>
{% endif %}
<main>
{% if user.is_authenticated or "accounts/login/" in request.path or "accounts/logout/" in request.path %}
<h1>{% block heading %}{% endblock %}</h1>
<div class="content">
{% block content %}{% endblock %}
</div>
{% else %}
<div class="centeringFlex">
{% translate "An error occured. Please log out and log in again." %}
<br>
<a href="/accounts/logout">log out</a>
</div>
{% endif %}
</main>
{% include "footer.html" %}
</div>
<script src="/static/js/main.js"></script>
</body>
</html>

View file

@ -0,0 +1,39 @@
{% extends "baseLayout.html" %}
{% load i18n %}
{% block title %}
{% translate "Drinks - Deposit" %}
{% endblock %}
{% block headAdditional %}
<link rel="stylesheet" href="/static/css/deposit.css">
{% endblock %}
{% block heading %}
{% translate "Deposit" %}
{% endblock %}
{% block content %}
<form id="depositForm">
{% csrf_token %}
<div class="row">
<div class="column">{% translate "Amount" %} {{ currency_suffix }}:</div>
<div class="column"><input type="number" name="depositAmount" id="depositAmount" max="9999.99" min="1.00"
step="0.01" autofocus></div>
</div>
<div id="statusInfo"></div>
<div class="horizontalButtonList">
<a href="/" class="button">{% translate "cancel" %}</a>
<input type="submit" id="depositSubmitBtn" class="button" value='{% translate "confirm" %}'>
</div>
</form>
<script src="/static/js/deposit.js"></script>
{% endblock %}

View file

@ -0,0 +1,6 @@
{% load i18n %}
<div class="footer">
<div>Version {{ app_version }}</div>
<div>Copyright (C) 2021, <a href="https://gitlab.com/W13R">Julian Müller (W13R)</a></div>
</div>

View file

@ -0,0 +1,5 @@
{% if global_message != "" %}
<div class="globalMessage">
<div>{{ global_message }}</div>
</div>
{% endif %}

View file

@ -0,0 +1,36 @@
{% extends "baseLayout.html" %}
{% load i18n %}
{% block title %}
{% translate "Drinks - History" %}
{% endblock %}
{% block headAdditional %}
<link rel="stylesheet" href="/static/css/history.css">
{% endblock %}
{% block heading %}
{% translate "History" %}
{% endblock %}
{% block content %}
{% if history %}
<table class="history">
<tr>
<th>{% translate "last 30 actions" %}</th>
<th></th>
</tr>
{% for h in history %}
<tr>
<td>{{ h.0 }}</td>
<td class="historyDate">{{ h.1 }}</td>
</tr>
{% endfor %}
</table>
{% else %}
{% translate "No history." %}
{% endif %}
{% endblock %}

View file

@ -0,0 +1,47 @@
{% extends "baseLayout.html" %}
{% load i18n %}
{% block title %}
{% translate "Drinks - Home" %}
{% endblock %}
{% block headAdditional %}
<link rel="stylesheet" href="/static/css/index.css">
{% endblock %}
{% block heading %}
{% translate "Available Drinks" %}
{% endblock %}
{% block content %}
{% if available_drinks %}
<ul class="availableDrinksList">
{% for drink in available_drinks %}
{% if drink.binary_availability %}
<li>
<a class="button" href="/order/{{ drink.id }}">
<span>{{ drink }}</span>
<span>{% translate "available" %}</span>
</a>
</li>
{% else %}
<li>
<a class="button" href="/order/{{ drink.id }}">
<span>{{ drink }}</span>
<span>{{ drink.available }} {% translate "available" %}</span>
</a>
</li>
{% endif %}
{% endfor %}
</ul>
{% else %}
{% translate "No drinks available." %}
{% endif %}
{% endblock %}

View file

@ -0,0 +1,99 @@
{% extends "baseLayout.html" %}
{% load i18n %}
{% block title %}
{% translate "Drinks - Order" %}
{% endblock %}
{% block headAdditional %}
<link rel="stylesheet" href="/static/css/order.css">
<link rel="stylesheet" href="/static/css/customNumberInput.css">
{% endblock %}
{% block heading %}
{% translate "Order" %}
{% endblock %}
{% block content %}
{% if drink and drink.available > 0 and not drink.deleted %}
{% if user.balance > 0 or user.allow_order_with_negative_balance %}
<form id="orderForm">
{% csrf_token %}
<div class="row">
<div class="column">{% translate "Drink" %}:</div>
<div class="column">{{ drink.product_name }}</div>
</div>
<div class="row">
<div class="column">{% translate "Price per Item" %} ({{ currency_suffix }}):</div>
<div class="column" id="pricePerDrink" data-drink-price="{{ drink.price }}">{{ drink.price }}</div>
</div>
{% if not drink.binary_availability %}
<div class="row">
<div class="column">{% translate "Available" %}:</div>
<div class="column">{{ drink.available }}</div>
</div>
{% endif %}
<div class="row">
<div class="column">{% translate "Count" %}:</div>
<div class="column">
<span class="customNumberInput">
<button type="button" class="customNumberInput-minus" id="numberOfDrinksBtnA">-</button>
{% if drink.binary_availability %}
<input type="number" class="customNumberInputField" name="numberOfDrinks" id="numberOfDrinks"
min="1" max="100" value="1">
{% else %}
<input type="number" class="customNumberInputField" name="numberOfDrinks" id="numberOfDrinks"
max="{{ drink.available }}" min="1" max="100" value="1">
{% endif %}
<button type="button" class="customNumberInput-plus" id="numberOfDrinksBtnB">+</button>
</span>
</div>
</div>
<div class="row">
<div class="column">{% translate "Sum" %} ({{ currency_suffix }}):</div>
<div class="column" id="orderCalculatedSum">{{ drink.price }}</div>
</div>
<div id="statusInfo"></div>
<input type="hidden" name="drinkID" id="drinkID" value="{{ drink.id }}">
<div class="horizontalButtonList">
<a href="/" class="button">{% translate "cancel" %}</a>
<input type="submit" id="orderSubmitBtn" class="button" value='{% translate "order" %}'>
</div>
</form>
<script src="/static/js/order.js"></script>
<script src="/static/js/customNumberInput.js"></script>
{% else %}
<div class="centeringFlex">
<p>{% translate "You can't order this, because you have a negative balance." %}</p>
<a href="/">{% translate "back" %}</a>
</div>
{% endif %}
{% else %}
<div class="centeringFlex">
<p>{% translate "This drink is not available." %}</p>
<a href="/">{% translate "back" %}</a>
</div>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,24 @@
{% extends "baseLayout.html" %}
{% load i18n %}
{% block title %}
{% translate "Drinks - Logged Out" %}
{% endblock %}
{% block headAdditional %}
<link rel="stylesheet" href="/static/css/login.css">
{% endblock %}
{% block content %}
<div class="centeringFlex">
{% translate "Logged out! You will be redirected shortly." %}
<br><br>
<a href="/">{% translate "Click here if automatic redirection does not work." %}</a>
</div>
<script src="/static/js/logged_out.js"></script>
{% endblock %}

View file

@ -0,0 +1,91 @@
{% extends "baseLayout.html" %}
{% load i18n %}
{% block title %}
{% translate "Drinks - Login" %}
{% endblock %}
{% block headAdditional %}
<link rel="stylesheet" href="/static/css/login.css">
{% endblock %}
{% block content %}
{% if error_message %}
<p class="errorText">{{ error_message }}</p>
{% endif %}
<div class="passwordOverlayContainer nodisplay" id="passwordOverlayContainer">
<div class="passwordOverlay">
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
<h1>{% translate "Log in" %}</h1>
<input type="text" name="username" autofocus="" autocapitalize="none" autocomplete="username" maxlength="150" required="" id="id_username">
<input type="password" name="password" autocomplete="current-password" required="" id="id_password" placeholder='{% translate "Password/PIN" %}'>
<div class="pinpad">
<table>
<tr>
<td><button type="button" class="pinpadBtn" data-btn="1">1</button></td>
<td><button type="button" class="pinpadBtn" data-btn="2">2</button></td>
<td><button type="button" class="pinpadBtn" data-btn="3">3</button></td>
</tr>
<tr>
<td><button type="button" class="pinpadBtn" data-btn="4">4</button></td>
<td><button type="button" class="pinpadBtn" data-btn="5">5</button></td>
<td><button type="button" class="pinpadBtn" data-btn="6">6</button></td>
</tr>
<tr>
<td><button type="button" class="pinpadBtn" data-btn="7">7</button></td>
<td><button type="button" class="pinpadBtn" data-btn="8">8</button></td>
<td><button type="button" class="pinpadBtn" data-btn="9">9</button></td>
</tr>
<tr>
<td><button type="button" class="pinpadBtn" data-btn="0">0</button></td>
<td><button type="button" class="pinpadBtn" data-btn="x">x</button></td>
<td><button type="button" class="pinpadBtn" data-btn="enter">&#9166;</button></td>
</tr>
</table>
</div>
<div class="horizontalButtonList">
<button type="button" id="pwoCancel">{% translate "cancel" %}</button>
<input class="button" id="submit_login" type="submit" value='{% translate "login" %}' />
</div>
</form>
</div>
</div>
<h1>{% translate "Choose your account" %}</h1>
<div class="userlistContainer">
<ul class="userlist">
{% for user_ in user_list %}
<li class="userlistButton button" data-username="{{ user_.username }}">
{% if user_.first_name %}
{{ user_.first_name }}
{% if user_.last_name %}
{{ user_.last_name }}
{% endif %}
{% else %}
{{ user_.username }}
{% endif %}
</li>
{% endfor %}
</ul>
</div>
<script src="/static/js/login.js"></script>
{% endblock %}

View file

@ -0,0 +1,179 @@
{% extends "baseLayout.html" %}
{% load i18n %}
{% block title %}
{% translate "Drinks - Statistics" %}
{% endblock %}
{% block headAdditional %}
<link rel="stylesheet" href="/static/css/statistics.css">
{% endblock %}
{% block heading %}
{% translate "Statistics" %}
{% endblock %}
{% block content %}
<div class="mainContainer">
<div class="dropDownMenu" id="statisticsDropDownMenu">
<button class="dropDownButton" id="statisticsDropDownMenuButton">
<div>
{% translate "Choose" %}
</div>
</button>
<div class="dropDownList">
<button class="sChoice dropDownChoice" data-statistics_div="noyopd">
{% translate "Your orders per drink" %}
</button>
<button class="sChoice dropDownChoice" data-statistics_div="yopwd">
{% translate "Your orders per weekday" %}
</button>
<button class="sChoice dropDownChoice" data-statistics_div="yopml12m">
{% translate "Your orders per month (last 12 months)" %}
</button>
<button class="sChoice dropDownChoice" data-statistics_div="noaopd">
{% translate "All orders per drink" %}
</button>
<button class="sChoice dropDownChoice" data-statistics_div="aopwd">
{% translate "All orders per weekday" %}
</button>
<button class="sChoice dropDownChoice" data-statistics_div="aopml12m">
{% translate "All orders per month (last 12 months)" %}
</button>
</div>
</div>
<div class="tablesContainer">
<div id="noyopd" class="statisticsTable nodisplay">
<h1>{% translate "Your orders per drink" %}</h1>
{% if noyopd %}
<table>
<tr>
<th>{% translate "drink" %}</th>
<th>{% translate "count" %}</th>
</tr>
{% for row in noyopd %}
<tr>
<td>{{ row.0 }}</td>
<td>{{ row.1 }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<div>{% translate "No history." %}</div>
{% endif %}
</div>
<div id="noaopd" class="statisticsTable nodisplay">
<h1>{% translate "All orders per drink" %}</h1>
{% if noaopd %}
<table>
<tr>
<th>{% translate "drink" %}</th>
<th>{% translate "count" %}</th>
</tr>
{% for row in noaopd %}
<tr>
<td>{{ row.0 }}</td>
<td>{{ row.1 }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<div>{% translate "No history." %}</div>
{% endif %}
</div>
<div id="yopml12m" class="statisticsTable nodisplay">
<h1>{% translate "Your orders per month (last 12 months)" %}</h1>
{% if yopml12m %}
<table>
<tr>
<th>{% translate "month" %}</th>
<th>{% translate "count" %}</th>
</tr>
{% for row in yopml12m %}
<tr>
<td>{{ row.0 }}</td>
<td>{{ row.1 }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<div>{% translate "No history." %}</div>
{% endif %}
</div>
<div id="aopml12m" class="statisticsTable nodisplay">
<h1>{% translate "All orders per month (last 12 months)" %}</h1>
{% if aopml12m %}
<table>
<tr>
<th>{% translate "month" %}</th>
<th>{% translate "count" %}</th>
</tr>
{% for row in aopml12m %}
<tr>
<td>{{ row.0 }}</td>
<td>{{ row.1 }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<div>{% translate "No history." %}</div>
{% endif %}
</div>
<div id="yopwd" class="statisticsTable nodisplay">
<h1>{% translate "Your orders per weekday" %}</h1>
{% if yopwd %}
<table>
<tr>
<th>{% translate "day" %}</th>
<th>{% translate "count" %}</th>
</tr>
{% for row in yopwd %}
<tr>
<td>{{ row.0 }}</td>
<td>{{ row.1 }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<div>{% translate "No history." %}</div>
{% endif %}
</div>
<div id="aopwd" class="statisticsTable nodisplay">
<h1>{% translate "All orders per weekday" %}</h1>
{% if aopwd %}
<table>
<tr>
<th>{% translate "day" %}</th>
<th>{% translate "count" %}</th>
</tr>
{% for row in aopwd %}
<tr>
<td>{{ row.0 }}</td>
<td>{{ row.1 }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<div>{% translate "No history." %}</div>
{% endif %}
</div>
</div>
</div>
<script src="/static/js/statistics.js"></script>
{% endblock %}

View file

@ -0,0 +1,31 @@
{% load i18n %}
<div class="dropDownMenu" id="dropDownMenu">
<button class="dropDownButton" id="dropDownMenuButton">
<div>
{% if user.first_name != "" %}
{% translate "User" %}: {{ user.first_name }} {{ user.last_name }} ({{ user.username }})
{% else %}
{% translate "User" %}: {{ user.username }}
{% endif %}
&nbsp;-&nbsp;
{% if user.balance < 0.01 %}
<span class="userBalanceWarn">{% translate "Balance" %}: {{ user.balance }}{{ currency_suffix }}</span>
{% else %}
<span>{% translate "Balance" %}: {{ user.balance }}{{ currency_suffix }}</span>
{% endif %}
</div>
</button>
<div class="dropDownList">
<a class="button dropDownChoice" id="navBarBtnHome" href="/">Home</a>
<a class="button dropDownChoice" id="navBarBtnHistory" href="/history">{% translate "History" %}</a>
<a class="button dropDownChoice" id="navBarBtnStatistics" href="/statistics">{% translate "Statistics" %}</a>
<a class="button dropDownChoice" id="navBarBtnDeposit" href="/deposit">{% translate "Deposit" %}</a>
{% if user.is_superuser %}
<a class="button dropDownChoice" href="/admin/">Admin Panel</a>
{% else %}
<a class="button dropDownChoice" href="/accounts/password_change/">{% translate "Change Password" %}</a>
{% endif %}
<a class="button dropDownChoice" href="/accounts/logout">{% translate "Logout" %}</a>
</div>
</div>

3
application/app/tests.py Normal file
View file

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

22
application/app/urls.py Normal file
View file

@ -0,0 +1,22 @@
from django.urls import path, include
from django.contrib.auth import views as auth_views
from . import views
from .admin import adminSite
urlpatterns = [
path('', views.index),
path('order/<drinkID>/', views.order),
path('history/', views.history),
path('deposit/', views.deposit),
path('statistics/', views.statistics),
path('accounts/login/', views.login_page, name="login"),
path('accounts/logout/', auth_views.LogoutView.as_view(), name='logout'),
path('accounts/password_change/', auth_views.PasswordChangeView.as_view(), name='password_change'),
path('accounts/password_change_done/', views.redirect_home, name='password_change_done'),
path('admin/', adminSite.urls),
# API #
path('api/order-drink', views.api_order_drink),
path('api/deposit', views.api_deposit),
#path('api/get-statistics', views.api_get_statistics)
]

167
application/app/views.py Normal file
View file

@ -0,0 +1,167 @@
import json
import sys
from django.contrib.auth import authenticate
from django.contrib.auth import get_user_model
from django.contrib.auth import login
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import AuthenticationForm
from django.http.response import HttpResponseRedirect
from django.http.response import HttpResponse
from django.shortcuts import render
from django.utils.translation import gettext as _
from django.utils.formats import decimal
from . import sql_queries
from .models import Drink
from .models import Order
from .models import RegisterTransaction
# login view
def login_page(request):
userlist = get_user_model().objects.filter(is_superuser=False).filter(is_active=True).order_by("username")
if request.method == "POST":
form = AuthenticationForm(request.POST)
username = request.POST['username']
password = request.POST['password']
user = authenticate(username=username,password=password)
if user:
if user.is_active:
login(request, user)
return HttpResponseRedirect("/")
else:
return render(request,'registration/login.html', {
"form": form,
"user_list": userlist,
"error_message": _("Invalid username or password.")
})
else:
if request.user.is_authenticated:
return HttpResponseRedirect("/")
form = AuthenticationForm()
return render(request,'registration/login.html', {
"form": form,
"user_list": userlist
})
# actual application
@login_required
def index(request):
context = {
"available_drinks": Drink.objects.filter(available__gt=0).filter(deleted=False),
}
return render(request, "index.html", context)
@login_required
def history(request):
context = {
"history": sql_queries.select_history(request.user, language_code=request.LANGUAGE_CODE),
}
return render(request, "history.html", context)
@login_required
def order(request, drinkID):
try:
drink_ = Drink.objects.get(pk=drinkID)
context = {
"drink": drink_
}
return render(request, "order.html", context)
except Drink.DoesNotExist:
return HttpResponseRedirect("/")
@login_required
def deposit(request):
return render(request, "deposit.html", {})
@login_required
def statistics(request):
context = {
"yopml12m": sql_queries.select_yopml12m(request.user),
"aopml12m": sql_queries.select_aopml12m(),
"yopwd": sql_queries.select_yopwd(request.user),
"aopwd": sql_queries.select_aopwd(),
"noyopd": sql_queries.select_noyopd(request.user),
"noaopd": sql_queries.select_noaopd()
}
return render(request, "statistics.html", context)
@login_required
def redirect_home(request):
return HttpResponseRedirect("/")
# API for XHR requests #
@login_required
def api_order_drink(request):
# check request -> make order
user = request.user
try:
if user.allow_order_with_negative_balance or user.balance > 0:
drinkID = int(request.POST["drinkID"])
amount = int(request.POST["numberOfDrinks"])
drink = Drink.objects.get(pk=drinkID)
if ((drink.binary_availability and drink.available > 0) or (drink.available >= amount)) and not drink.deleted:
Order.objects.create(drink=drink, user=user, amount=amount)
return HttpResponse("success", status=200)
else:
return HttpResponse("notAvailable", status=400)
else: raise Exception("Balance below zero.")
except Exception as e:
print(f"An exception occured while processing an order: User: {user.username} - Exception: {e}", file=sys.stderr)
return HttpResponse(b"", status=500)
@login_required
def api_deposit(request):
# check request -> deposit
user = request.user
try:
amount = decimal.Decimal(request.POST["depositAmount"])
if 0.00 < amount < 9999.99:
# create transaction
RegisterTransaction.objects.create(
transaction_sum=amount,
comment=f"User deposit by user {user.username}",
is_user_deposit=True,
user=user
)
#
return HttpResponse("success", status=200)
else: raise Exception("Deposit amount too big or small.")
except Exception as e:
print(f"An exception occured while processing an transaction: User: {user.username} - Exception: {e}", file=sys.stderr)
return HttpResponse(b"", status=500)