Merge branch 'release/1.0.0'

This commit is contained in:
Robert Einsle 2019-02-15 18:09:50 +01:00
commit d042f8ebed
55 changed files with 11692 additions and 10787 deletions

2
.gitignore vendored
View File

@ -8,3 +8,5 @@ build/
*/*/__pycache__/ */*/__pycache__/
caromserver/local_settings.py caromserver/local_settings.py
/staticfiles/ /staticfiles/
.venv/
update.sh

18
Pipfile Normal file
View File

@ -0,0 +1,18 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
django = "==2.1.5"
django-crispy-forms = "==1.7.2"
django-debug-toolbar = "==1.11"
django-extensions = "==2.1.5"
django-tables2 = "==2.0.4"
djangorestframework = "==3.9.1"
requests = "==2.21.0"
[requires]
python_version = "3.5"

126
Pipfile.lock generated Normal file
View File

@ -0,0 +1,126 @@
{
"_meta": {
"hash": {
"sha256": "de1e65bd4b2342db22fea58996979f48314fcad56bd7b50f4c939035771ef85c"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.5"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"certifi": {
"hashes": [
"sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7",
"sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"
],
"version": "==2018.11.29"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"django": {
"hashes": [
"sha256:a32c22af23634e1d11425574dce756098e015a165be02e4690179889b207c7a8",
"sha256:d6393918da830530a9516bbbcbf7f1214c3d733738779f06b0f649f49cc698c3"
],
"index": "pypi",
"version": "==2.1.5"
},
"django-crispy-forms": {
"hashes": [
"sha256:5952bab971110d0b86c278132dae0aa095beee8f723e625c3d3fa28888f1675f",
"sha256:705ededc554ad8736157c666681165fe22ead2dec0d5446d65fc9dd976a5a876"
],
"index": "pypi",
"version": "==1.7.2"
},
"django-debug-toolbar": {
"hashes": [
"sha256:89d75b60c65db363fb24688d977e5fbf0e73386c67acf562d278402a10fc3736",
"sha256:c2b0134119a624f4ac9398b44f8e28a01c7686ac350a12a74793f3dd57a9eea0"
],
"index": "pypi",
"version": "==1.11"
},
"django-extensions": {
"hashes": [
"sha256:6fcedb2ea660c9dbf9ac59441721ffdd4ab5b753fbd6159c3e28f391a65bab46",
"sha256:a607459e5fa8c579a672131b63366fa52fab80adb2a862d362f5fb48cd2d2cac"
],
"index": "pypi",
"version": "==2.1.5"
},
"django-tables2": {
"hashes": [
"sha256:a893fca1afe2e95b9739c6428cc6c9735a219f65707e24274df3920f61358525",
"sha256:b5f7b4c76160ee927005e52ebea633c86d4529cf84757c0acd5d0434d31798a1"
],
"index": "pypi",
"version": "==2.0.4"
},
"djangorestframework": {
"hashes": [
"sha256:79c6efbb2514bc50cf25906d7c0a5cfead714c7af667ff4bd110312cd380ae66",
"sha256:a4138613b67e3a223be6c97f53b13d759c5b90d2b433bad670b8ebf95402075f"
],
"index": "pypi",
"version": "==3.9.1"
},
"idna": {
"hashes": [
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
],
"version": "==2.8"
},
"pytz": {
"hashes": [
"sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9",
"sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c"
],
"version": "==2018.9"
},
"requests": {
"hashes": [
"sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
"sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
],
"index": "pypi",
"version": "==2.21.0"
},
"six": {
"hashes": [
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
],
"version": "==1.12.0"
},
"sqlparse": {
"hashes": [
"sha256:ce028444cfab83be538752a2ffdb56bc417b7784ff35bb9a3062413717807dec",
"sha256:d9cf190f51cbb26da0412247dfe4fb5f4098edb73db84e02f9fc21fdca31fed4"
],
"version": "==0.2.4"
},
"urllib3": {
"hashes": [
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
"sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
],
"version": "==1.24.1"
}
},
"develop": {}
}

View File

@ -35,6 +35,12 @@ class ClientAdmin(admin.ModelAdmin):
fields = ['location', 'uuid', 'report_user', 'last_seen'] fields = ['location', 'uuid', 'report_user', 'last_seen']
@admin.register(ClientData)
class ClientDataAdmin(admin.ModelAdmin):
list_display = ('uuid', 'last_seen')
fields = ['location', 'last_seen']
@admin.register(LocationData) @admin.register(LocationData)
class LocationDataAdmin(admin.ModelAdmin): class LocationDataAdmin(admin.ModelAdmin):
def get_urls(self): def get_urls(self):
@ -46,6 +52,8 @@ class LocationDataAdmin(admin.ModelAdmin):
return my_urls + urls return my_urls + urls
def process_locationdata(self, request): def process_locationdata(self, request):
from .tasks import process_location_data
process_location_data(sender=None)
messages.success(request, 'Items processed.') messages.success(request, 'Items processed.')
return redirect('admin:billard_locationdata_changelist') return redirect('admin:billard_locationdata_changelist')

10
billard/forms.py Normal file
View File

@ -0,0 +1,10 @@
from django import forms
from django.contrib.auth.models import User
class UserInformationUpdateForm(forms.ModelForm):
email = forms.EmailField()
class Meta:
model = User
fields = ('first_name', 'last_name', 'email',)

View File

@ -0,0 +1,24 @@
# Generated by Django 2.0.2 on 2018-02-19 10:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('billard', '0026_client_last_seen'),
]
operations = [
migrations.CreateModel(
name='ClientData',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(verbose_name='Identifier')),
('last_seen', models.DateTimeField(verbose_name='Letzter Update')),
],
options={
'verbose_name': 'Client Data logs',
'verbose_name_plural': 'Client Data logs',
},
),
]

View File

@ -4,8 +4,6 @@ import uuid
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import models from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -102,7 +100,13 @@ class Accounting(models.Model):
verbose_name_plural = "Buchhaltungseinträge" verbose_name_plural = "Buchhaltungseinträge"
@receiver(post_save, sender=LocationData) class ClientData(models.Model):
def test(sender, **kwargs): uuid = models.UUIDField(verbose_name="Identifier")
from .tasks import process_location_data last_seen = models.DateTimeField(verbose_name="Letzter Update")
process_location_data()
def __str__(self):
return '{}, {}'.format(self.uuid, self.last_seen)
class Meta:
verbose_name = "Client Data logs"
verbose_name_plural = "Client Data logs"

View File

@ -1,6 +1,6 @@
from rest_framework import serializers from rest_framework import serializers
from billard.models import LocationData, Client from billard.models import LocationData, ClientData
class LocationDataSerializer(serializers.HyperlinkedModelSerializer): class LocationDataSerializer(serializers.HyperlinkedModelSerializer):
@ -11,5 +11,5 @@ class LocationDataSerializer(serializers.HyperlinkedModelSerializer):
class ClientUpdateLastSeenSerializer(serializers.HyperlinkedModelSerializer): class ClientUpdateLastSeenSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = Client model = ClientData
fields = ('uuid', 'last_seen') fields = ('uuid', 'last_seen')

22
billard/tables.py Normal file
View File

@ -0,0 +1,22 @@
import django_tables2 as tables
from .models import Location
class LocationTable(tables.Table):
code = tables.TemplateColumn(template_name='billard/tc_location_detail.html')
class Meta:
model = Location
fields = ('code', 'name', 'street', 'plz', 'city')
orderable = False
class LocationAccountingTable(tables.Table):
code = tables.TemplateColumn(template_name='billard/tc_location_detail.html')
accounting = tables.TemplateColumn(template_name='billard/tc_accounting_detail.html')
class Meta:
model = Location
fields = ('code', 'name', 'street', 'plz', 'city', 'accounting')
orderable = False

View File

@ -2,13 +2,28 @@ from __future__ import absolute_import, unicode_literals
import logging import logging
from django.db.models.signals import post_save
from django.dispatch import receiver
import billard.utils as utils import billard.utils as utils
from billard.models import LocationData, Client, Accounting from billard.models import LocationData, Client, Accounting, ClientData
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def process_location_data(): @receiver(post_save, sender=ClientData)
def process_client_data(sender, **kwargs):
data = ClientData.objects.all().order_by('last_seen')
for cd in data:
client = Client.objects.get(uuid=cd.uuid)
client.last_seen = cd.last_seen
client.save()
cd.delete()
@receiver(post_save, sender=LocationData)
def process_location_data(sender, **kwargs):
log.info('Starte die Verarbeitung der Location-Data-Objecte')
data = LocationData.objects.filter(processed=False).order_by('tst') data = LocationData.objects.filter(processed=False).order_by('tst')
for ld in data: for ld in data:
try: try:
@ -64,3 +79,10 @@ def process_location_data():
log.error(ld.error_msg) log.error(ld.error_msg)
except: except:
log.exception('', exc_info=True) log.exception('', exc_info=True)
@receiver(post_save, sender=Accounting)
def process_accounting_data(sender, **kwargs):
log.info('Starte die Verarbeitung der Accounting-Data-Objecte')
data = Accounting.objects.filter(prize=0.0, reporter_uuid__isnull=True).exclude(time_to__isnull=True)
data.delete()

View File

@ -2,7 +2,7 @@
{% load i18n admin_urls static admin_list %} {% load i18n admin_urls static admin_list %}
{% block object-tools-items %} {% block object-tools-items %}
<li> <li>
<a href="{% url 'admin:process_locationdata' %}"> <a href="{% url 'admin:process_locationdata' %}" title="Verarbeiten der LocationDate Elemente">
LD Verarbeiten LD Verarbeiten
</a> </a>
</li> </li>

View File

@ -15,8 +15,12 @@
{{ pk }} {{ pk }}
<form action="confirm/" method="post" id="accounting">
{% csrf_token %}
<table class="table"> <table class="table">
<tr> <tr>
<th></th>
<th>Start-Datum:</th> <th>Start-Datum:</th>
<th>Stop-Datum:</th> <th>Stop-Datum:</th>
<th>Preis Normal:</th> <th>Preis Normal:</th>
@ -25,6 +29,8 @@
</tr> </tr>
{% for acc in accounting %} {% for acc in accounting %}
<tr> <tr>
<td><input type="checkbox" name="list_acc_id" id="option{{ acc.id }}"
value={{ acc.id }} checked="checked"/></td>
<td>{{ acc.time_from }}</td> <td>{{ acc.time_from }}</td>
<td>{{ acc.time_to }}</td> <td>{{ acc.time_to }}</td>
<td>{{ acc.prize_normal }}</td> <td>{{ acc.prize_normal }}</td>
@ -33,12 +39,9 @@
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
<form action="confirm/" method="post" id="accounting">
{% csrf_token %}
<input type="hidden" name="location-selector" value="{{ location_id }}"> <input type="hidden" name="location-selector" value="{{ location_id }}">
<input type="hidden" name="accountings" value="{{ acc_ids }}"> <input type="hidden" name="accountings" value="{{ acc_ids }}">
<button type="submit" class="btn btn-default">Abrechnen</button> <button type="submit" class="btn btn-danger">Abrechnen</button>
</form> </form>
{% endblock %} {% endblock %}

View File

@ -1,49 +0,0 @@
{% extends '_base.html' %}
{% load display_client %}
{% block title %}Location Data{% endblock %}
{% block content %}
{% if not locations|length_is:"1" %}
<form action="." method="post" id="location-form">
{% csrf_token %}
<div id="location-selector" class="alert">
<select class="form-control" form="location-form" name="location-selector" id="location-select">
{% for loc in locations %}
<option value="{{ loc.id }}"{% if loc.id == location_id %} selected{% endif %}>{{ loc.code }}
- {{ loc.name }}</option>
{% endfor %}
</select>
</div>
</form>
{% endif %}
<div id="desk_data">
{% include 'billard/index_ajax.html' %}
</div>
<div id="modal-wrapper">
</div>
{% endblock %}
{% block js %}
<script type="text/javascript">
var interval;
$(document).ready(function () {
$.ajaxSetup({cache: false});
interval = window.setInterval(refresh_page, 1000);
});
function refresh_page() {
$('#desk_data').load('#');
$('#modal-wrapper').load('{% url 'accountmodal' %}', function () {
if ($('#accountsmodal').length) {
window.clearInterval(interval);
$('#accountsmodal').modal('show');
}
});
}
</script>
{% endblock %}

View File

@ -1,10 +0,0 @@
{% load display_client %}
{% if clients %}
{% for cli in clients %}
{% for i in range %} {{ cli|display_client:i }} {% endfor %}
{% endfor %}
{% else %}
<div class="col-md-12">
<div class="col-md-12 alert alert-danger">Keine Tische angelegt!</div>
</div>
{% endif %}

View File

@ -1,4 +1,4 @@
{% load display_client %} {% load display_client display_daily_sale %}
{% if location.clients.all %} {% if location.clients.all %}
{% for cli in location.clients.all %} {% for cli in location.clients.all %}
{% for i in "12345678" %} {% for i in "12345678" %}
@ -10,3 +10,8 @@
<div class="alert alert-danger">Keine Tische angelegt!</div> <div class="alert alert-danger">Keine Tische angelegt!</div>
</div> </div>
{% endif %} {% endif %}
<div class="col col-12">
<div class="alert alert-warning">
{{ location|display_daily_sale }}
</div>
</div>

View File

@ -1,4 +1,5 @@
{% extends '_base.html' %} {% extends '_base.html' %}
{% load render_table from django_tables2 %}
{% block title %}Standortliste{% endblock %} {% block title %}Standortliste{% endblock %}
@ -7,37 +8,6 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% if location_list %}
<h2>Bitte Standort auswählen:</h2> <h2>Bitte Standort auswählen:</h2>
<table class="table table-hover"> {% render_table table %}
<tr>
<th>Code</th>
<th>Name</th>
<th>Strasse</th>
<th>Plz</th>
<th>Ort</th>
{% if perms.billard.change_accounting %}
<th>Accounting</th>
{% endif %}
</tr>
{% for loc in location_list %}
<tr>
<td><a href="{% url 'billard:location_detail' loc.id %}"
class="btn btn-outline-primary btn-sm">{{ loc.code|default_if_none:"" }}</a></td>
<td>{{ loc.name|default_if_none:"" }}</td>
<td>{{ loc.street|default_if_none:"" }}</td>
<td>{{ loc.plz|default_if_none:"" }}</td>
<td>{{ loc.city|default_if_none:"" }}</td>
{% if perms.billard.change_accounting %}
<td><a href="{% url 'billard:accounting_detail' loc.id %}"
class="btn btn-outline-danger btn-sm">Abrechnen</a></td>
{% endif %}
</tr>
{% endfor %}
</table>
{% else %}
<p>Keine Standorte Zugeordnet.</p>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -1,17 +0,0 @@
{% extends '_base.html' %}
{% block title %}Location Data{% endblock %}
{% block content %}
{% include 'billard/locationdata_detail_ajax.html' %}
{% endblock %}
{% block js %}
<script type="text/javascript">
$(document).ready(function () {
setInterval(function () {
$("#content").load("#")
}, 1000);
});
</script>
{% endblock %}

View File

@ -1,51 +0,0 @@
<h1>Locationdata: {{ locationdata.id }}</h1>
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
<form action="{% url 'index' %}" method="post" class="form-horizontal">
{% csrf_token %}
<div class="form-group">
<label for="location_id" class="col-sm-2 control-label">Location Id</label>
<div class="col-sm-10">
<input id="location_id" type="text" name="location_id" value="{{ locationdata.location_id }}"
placeholder="Locaton" class="form-control" disabled="disabled"/>
</div>
<div class="form-group">
<label for="table_no" class="col-sm-2 control-label">Table Number</label>
<div class="col-sm-10">
<input id="table_no" type="number" name="table_no" value="{{ locationdata.table_no }}"
placeholder="Table" class="form-control" min="1" max="8" disabled="disabled"/>
</div>
<div class="form-group">
<label for="tst" class="col-sm-2 control-label">Timestamp</label>
<div class="col-sm-10">
<input id="tst" type="datetime" name="tst" value="{{ locationdata.tst }}"
placeholder="Table" class="form-control" disabled="disabled"/>
</div>
</div>
<div class="form-group">
<label for="on_off" class="col-sm-2 control-label">On / Off</label>
<div class="col-sm-10">
<input id="on_off" type="checkbox" name="on_off" value="{{ locationdata.on_off }}"
placeholder="Table" class="form-control" disabled="disabled"/>
</div>
</div>
<div class="form-group">
<label for="processed" class="col-sm-2 control-label">Processed</label>
<div class="col-sm-10">
<input id="processed" type="checkbox" name="processed" value="{{ locationdata.processed }}"
placeholder="Table" class="form-control" disabled="disabled"/>
</div>
</div>
<div class="form-group">
<label for="error_msg" class="col-sm-2 control-label">Error Message</label>
<div class="col-sm-10">
<input id="error_msg" type="text" name="error_msg" value="{{ locationdata.error_msg }}"
placeholder="Table" class="form-control" disabled="disabled"/>
</div>
</div>
<button type="submit" class="btn btn-default">Abschicken</button>
<a class="btn btn-default" href=".." role="button">Zurück</a>
</form>

View File

@ -1,17 +0,0 @@
{% extends '_base.html' %}
{% block title %}Location Data{% endblock %}
{% block content %}
{% include 'billard/locationdata_list_ajax.html' %}
{% endblock %}
{% block js %}
<script type="text/javascript">
$(document).ready(function () {
setInterval(function () {
$("#content").load("#")
}, 1000);
});
</script>
{% endblock %}

View File

@ -1,42 +0,0 @@
{% extends '_base.html' %}
{% block title %}Location Data{% endblock %}
{% block content %}
<div id="location-selector" class="alert">
<select class="form-control">
<option value="1">Casino 1</option>
<option value="2">Casino 2</option>
</select>
</div>
{% if object_list %}
<h1>Location Data</h1>
<table class="table table-hover">
<tr>
<th>ID</th>
<th>Location</th>
<th>Table</th>
<th>Timestamp</th>
<th>On_Off</th>
<th>Proc</th>
<th>Error</th>
</tr>
{% for location_data in object_list %}
<tr>
<td><a href="{% url 'detail' location_data.id %}">{{ location_data.id }}</a></td>
<td>{{ location_data.location_id }}</td>
<td>{{ location_data.table_no }}</td>
<td>{{ location_data.tst }}</td>
<td>{{ location_data.on_off }}</td>
<td>{{ location_data.processed }}</td>
<td>{{ location_data.error_msg }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<p>No data available.</p>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,2 @@
{% load static from staticfiles %}
<a href="{% url 'billard:accounting_detail' record.id %}" class="btn btn-outline-danger btn-sm">Abrechnen</a>

View File

@ -0,0 +1,3 @@
{% load static from staticfiles %}
<a href="{% url 'billard:location_detail' record.id %}"
class="btn btn-outline-primary btn-sm">{{ record.code|default_if_none:"" }}</a>

View File

@ -17,28 +17,10 @@ def display_client(client, desk_no):
loc = desk.client.location loc = desk.client.location
if not desk.enabled: if not desk.enabled:
return '' return ''
alert = 'alert-success'
acc = desk.accounting_set.all()[:3][::-1] acc = desk.accounting_set.all()[:3][::-1]
if acc is not None and len(acc) > 0: _calc_prize(desk, acc)
a = acc[-1]
if a.time_to is None:
alert = 'alert-info'
prize, u1, u2 = utils.get_prize_for(
start=a.time_from,
end=datetime.now(),
pph=desk.prize,
hh_start=loc.happy_hour_start,
hh_end=loc.happy_hour_end,
pphh=desk.prize_hh,
)
prize = '{0:.2f}'.format(prize)
if prize != a.prize:
a.prize = prize
before5min = datetime.now() - timedelta(minutes=5)
if client.last_seen is not None and client.last_seen < before5min:
alert = 'alert-danger'
html = '<div class="col col-12 col-lg-6">\n' html = '<div class="col col-12 col-lg-6">\n'
html += ' <div class="table-info alert {}">\n'.format(alert) html += ' <div class="table-info alert {}">\n'.format(_get_alert_name(desk))
html += ' <h4 style="text-align: center">({}) {}</h4>\n'.format(desk_no, desk.name) html += ' <h4 style="text-align: center">({}) {}</h4>\n'.format(desk_no, desk.name)
if loc.happy_hour_start is not None and desk.prize_hh is not None: if loc.happy_hour_start is not None and desk.prize_hh is not None:
html += ' <h6 style="text-align: center">Preis: {:.2f} € / Stunde | {} - {}: {:.2f} / € Stunde</h6>\n' \ html += ' <h6 style="text-align: center">Preis: {:.2f} € / Stunde | {} - {}: {:.2f} / € Stunde</h6>\n' \
@ -67,3 +49,33 @@ def display_client(client, desk_no):
html += '</div>\n' html += '</div>\n'
html = format_html(html) html = format_html(html)
return html return html
def _get_alert_name(desk):
alert = 'alert-success'
acc = desk.accounting_set.all()[:3][::-1]
if acc is not None and len(acc) > 0:
a = acc[-1]
if a.time_to is None:
alert = 'alert-info'
before5min = datetime.now() - timedelta(minutes=5)
if desk.client.last_seen is not None and desk.client.last_seen < before5min:
alert = 'alert-danger'
return alert
def _calc_prize(desk, acc):
if acc is not None and len(acc) > 0:
a = acc[-1]
if a.time_to is None:
prize, u1, u2 = utils.get_prize_for(
start=a.time_from,
end=datetime.now(),
pph=desk.prize,
hh_start=desk.client.location.happy_hour_start,
hh_end=desk.client.location.happy_hour_end,
pphh=desk.prize_hh,
)
prize = '{0:.2f}'.format(prize)
if prize != a.prize:
a.prize = prize

View File

@ -0,0 +1,19 @@
from datetime import datetime, timedelta
from django import template
from django.db.models import Sum
from billard.models import Accounting
register = template.Library()
@register.filter(is_safe=True)
def display_daily_sale(location):
start_date = datetime.now().replace(hour=5, minute=0, second=0, microsecond=0)
end_date = start_date + timedelta(days=1)
prize__sum = Accounting.objects.filter(desk__client__location=location,
time_to__range=(start_date, end_date)).aggregate(Sum('prize'))
if prize__sum['prize__sum'] is None:
prize__sum['prize__sum'] = 0
return "Tagesumsatz: {0:.2f} EUR".format(prize__sum['prize__sum'])

View File

@ -25,6 +25,6 @@ urlpatterns = [
path('<int:loc_pk>/account_modal/<pks>/confirm/', views.account_modal_confirm_view, name='account_modal_confirm'), path('<int:loc_pk>/account_modal/<pks>/confirm/', views.account_modal_confirm_view, name='account_modal_confirm'),
# ex. /billard/api/v1/ (rest api) # ex. /billard/api/v1/ (rest api)
path('api/v1/', include(router.urls)), path('api/v1/', include(router.urls)),
# ex. /billard/process_location_data/ # ex. /billard/myaccount/
path('process_location_data/', views.process_location_data, name='process_location_data'), path('my_account', views.UserUpdateView.as_view(), name='my_account'),
] ]

View File

@ -1,23 +1,25 @@
import ast
import logging import logging
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Sum from django.db.models import Sum
from django.http import HttpResponse
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.urls import reverse_lazy
from django.utils import timezone from django.utils import timezone
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views import generic from django.views import generic
from django.views.generic import UpdateView
from rest_framework import viewsets from rest_framework import viewsets
from billard.models import LocationData, Location, Client, Accounting from billard.models import LocationData, Location, Client, Accounting
from billard.serializers import LocationDataSerializer, ClientUpdateLastSeenSerializer from billard.serializers import LocationDataSerializer, ClientUpdateLastSeenSerializer
from billard.tasks import process_location_data from .forms import UserInformationUpdateForm
from .tables import LocationTable, LocationAccountingTable
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class LocationIndexView(generic.ListView): class LocationIndexView(LoginRequiredMixin, generic.ListView):
template_name = 'billard/location_index.html' template_name = 'billard/location_index.html'
context_object_name = 'location_list' context_object_name = 'location_list'
@ -25,8 +27,17 @@ class LocationIndexView(generic.ListView):
"""Return the last five published questions.""" """Return the last five published questions."""
return Location.objects.filter(users__id=self.request.user.id).order_by('code') return Location.objects.filter(users__id=self.request.user.id).order_by('code')
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs)
table = LocationTable(self.get_queryset())
user = self.request.user
if user.has_perm('billard.change_accounting'):
table = LocationAccountingTable(self.get_queryset())
context['table'] = table
return context
class LocationDetailView(generic.DetailView):
class LocationDetailView(LoginRequiredMixin, generic.DetailView):
model = Location model = Location
template_name = 'billard/location_detail.html' template_name = 'billard/location_detail.html'
@ -67,7 +78,7 @@ class AccountingView(generic.ListView):
def accounting_confirm(request, pk): def accounting_confirm(request, pk):
if request.method == 'POST': if request.method == 'POST':
if 'accountings' in request.POST: if 'accountings' in request.POST:
acc_ids = ast.literal_eval(request.POST['accountings']) acc_ids = request.POST.getlist('list_acc_id')
if len(acc_ids) > 0: if len(acc_ids) > 0:
Accounting.objects.filter(id__in=acc_ids).update( Accounting.objects.filter(id__in=acc_ids).update(
billed=True, billed=True,
@ -113,6 +124,11 @@ class ClientUpdateLastSeenViewSet(viewsets.ModelViewSet):
serializer_class = ClientUpdateLastSeenSerializer serializer_class = ClientUpdateLastSeenSerializer
def process_location_data(request): @method_decorator(login_required, name='dispatch')
process_location_data() class UserUpdateView(UpdateView):
return HttpResponse('DONE') form_class = UserInformationUpdateForm
template_name = 'registration/my_account.html'
success_url = reverse_lazy('billard:my_account')
def get_object(self):
return self.request.user

View File

@ -35,10 +35,13 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
# third party apps
'crispy_forms',
'debug_toolbar',
'django_tables2',
'rest_framework', 'rest_framework',
'rest_framework.authtoken', 'rest_framework.authtoken',
'debug_toolbar', # carom apps
'crispy_forms',
'billard', 'billard',
] ]
@ -160,10 +163,12 @@ EMAIL_PORT = 25
URL_LOCATION_PROCESSOR = 'http://127.0.0.1:8000/billard/process_locationdata' URL_LOCATION_PROCESSOR = 'http://127.0.0.1:8000/billard/process_locationdata'
PRODUCT_INFO = 'CAROM-DEV' PRODUCT_INFO = 'CAROM-DEV'
PRODUCT_VERSION = 'v 0.5.1' PRODUCT_VERSION = 'v 1.0.0'
INTERNAL_IPS = ['127.0.0.1'] INTERNAL_IPS = ['127.0.0.1']
DJANGO_TABLES2_TEMPLATE = 'django_tables2/bootstrap4.html'
try: try:
from local_settings import * from local_settings import *
except ImportError: except ImportError:

View File

@ -24,8 +24,6 @@ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
path('billard/', include('billard.urls')), path('billard/', include('billard.urls')),
path('login/', auth_views.login, name='login'),
path('logout/', auth_views.logout, name='logout'),
path('', include('django.contrib.auth.urls')), path('', include('django.contrib.auth.urls')),
path('', RedirectView.as_view(url='billard/', permanent=False), name='index') path('', RedirectView.as_view(url='billard/', permanent=False), name='index')
] ]

View File

@ -1,6 +0,0 @@
Django<2.1
django-crispy-forms==1.7.0
django-extensions>=1.7.0
djangorestframework>=3.6.0
requests>=2.18.0
django-debug-toolbar<2.0.0

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,330 +0,0 @@
/*!
* Bootstrap Reboot v4.0.0 (https://getbootstrap.com)
* Copyright 2011-2018 The Bootstrap Authors
* Copyright 2011-2018 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
font-family: sans-serif;
line-height: 1.15;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
-ms-overflow-style: scrollbar;
-webkit-tap-highlight-color: transparent;
}
@-ms-viewport {
width: device-width;
}
article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section {
display: block;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
text-align: left;
background-color: #fff;
}
[tabindex="-1"]:focus {
outline: 0 !important;
}
hr {
box-sizing: content-box;
height: 0;
overflow: visible;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 0.5rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-original-title] {
text-decoration: underline;
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
border-bottom: 0;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: .5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
dfn {
font-style: italic;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 80%;
}
sub,
sup {
position: relative;
font-size: 75%;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -.25em;
}
sup {
top: -.5em;
}
a {
color: #007bff;
text-decoration: none;
background-color: transparent;
-webkit-text-decoration-skip: objects;
}
a:hover {
color: #0056b3;
text-decoration: underline;
}
a:not([href]):not([tabindex]) {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):focus {
outline: 0;
}
pre,
code,
kbd,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
pre {
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
-ms-overflow-style: scrollbar;
}
figure {
margin: 0 0 1rem;
}
img {
vertical-align: middle;
border-style: none;
}
svg:not(:root) {
overflow: hidden;
}
table {
border-collapse: collapse;
}
caption {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
color: #6c757d;
text-align: left;
caption-side: bottom;
}
th {
text-align: inherit;
}
label {
display: inline-block;
margin-bottom: .5rem;
}
button {
border-radius: 0;
}
button:focus {
outline: 1px dotted;
outline: 5px auto -webkit-focus-ring-color;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
input {
overflow: visible;
}
button,
select {
text-transform: none;
}
button,
html [type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
padding: 0;
border-style: none;
}
input[type="radio"],
input[type="checkbox"] {
box-sizing: border-box;
padding: 0;
}
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"] {
-webkit-appearance: listbox;
}
textarea {
overflow: auto;
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
display: block;
width: 100%;
max-width: 100%;
padding: 0;
margin-bottom: .5rem;
font-size: 1.5rem;
line-height: inherit;
color: inherit;
white-space: normal;
}
progress {
vertical-align: baseline;
}
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
[type="search"] {
outline-offset: -2px;
-webkit-appearance: none;
}
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
summary {
display: list-item;
cursor: pointer;
}
template {
display: none;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

View File

@ -1,8 +0,0 @@
/*!
* Bootstrap Reboot v4.0.0 (https://getbootstrap.com)
* Copyright 2011-2018 The Bootstrap Authors
* Copyright 2011-2018 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.min.css.map */

File diff suppressed because one or more lines are too long

2520
static/css/bootstrap.css vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2103
static/js/bootstrap.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

2
static/js/jquery-3.3.1.slim.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -36,11 +36,6 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'billard:location_index' %}">Standorte</a> <a class="nav-link" href="{% url 'billard:location_index' %}">Standorte</a>
</li> </li>
{% if user.is_superuser %}
<li class="nav-item">
<a class="nav-link" href="/admin">Administration</a>
</li>
{% endif %}
</ul> </ul>
{% if user.is_authenticated %} {% if user.is_authenticated %}
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
@ -50,8 +45,12 @@
{{ user.username }} {{ user.username }}
</a> </a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="userMenu"> <div class="dropdown-menu dropdown-menu-right" aria-labelledby="userMenu">
<!-- <a class="dropdown-item" href="#">My account</a> --> <a class="dropdown-item" href="{% url 'billard:my_account' %}">My account</a>
<a class="disabled dropdown-item" href="#">{% settings_value "PRODUCT_VERSION" %}</a> {% if user.is_superuser %}
<a class="dropdown-item" href="/admin">Administration</a>
{% endif %}
<a class="disabled dropdown-item"
href="#">{% settings_value "PRODUCT_VERSION" %}</a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" href="{% url 'logout' %}">Log out</a> <a class="dropdown-item" href="{% url 'logout' %}">Log out</a>
</div> </div>

View File

@ -1,23 +1,28 @@
{% extends '_base_accounts.html' %} {% extends '_base.html' %}
{% load static from staticfiles %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% load form_tags %}
{% block title %}login{% endblock %} {% block title %}Anmelden{% endblock %}
{% block content %} {% block content %}
<div class="row justify-content-center"> <div class="row justify-content-center" style="margin-top: 80px;">
<div class="col-lg-4 col-md-6 col-sm-8"> <div class="col-lg-4 col-md-6 col-sm-8">
<div class="card">
<div class="card-body">
<h3 class="card-title">Log in</h3>
<form method="post" novalidate> <form method="post" novalidate>
{% csrf_token %} {% csrf_token %}
<div class="card">
<div class="card-header text-center">
<h3>{% settings_value "PRODUCT_INFO" %}</h3>
</div>
<div class="card-body">
<h3 class="card-title">Login</h3>
<input type="hidden" name="next" value="{{ next }}"> <input type="hidden" name="next" value="{{ next }}">
{{ form|crispy }} {{ form|crispy }}
</div>
<div class="card-footer">
<button type="submit" class="btn btn-primary btn-block">Log in</button> <button type="submit" class="btn btn-primary btn-block">Log in</button>
</div>
</div>
</form> </form>
</div> </div>
</div> </div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,21 @@
{% extends '_base.html' %}
{% load crispy_forms_tags %}
{% block title %}My account{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item active">My account</li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-6 col-md-8 col-sm-10">
<form method="post" novalidate>
{% csrf_token %}
{{ form|crispy }}
<button type="submit" class="btn btn-success">Save changes</button>
</form>
</div>
</div>
{% endblock %}