diff --git a/.gitignore b/.gitignore index 280fb6b..005984e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ /data/* /data/logs/* /data/tls/* -/data/django_static/* +/data/static/* /data/profilepictures/* /data/archive/* !/data/logs/ diff --git a/LICENSE b/LICENSE index 58eccf7..051bcde 100644 --- a/LICENSE +++ b/LICENSE @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/README.md b/README.md index 74a7248..490c76a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Drinks Manager +# Drinks Manager (Season 3) Note: This software is tailored to my own needs. I probably won't accept feature requests, and don't recommend you @@ -75,346 +75,3 @@ or ``` ./start.sh --devel ``` - -# Third-Party Licenses - -This software contains third-party software and resources. -These are listed here with their respective licenses. - -## Simple Keyboard - -Source: https://github.com/hodgef/simple-keyboard - -``` -MIT License - -Copyright (c) 2019 Francisco Hodge and project contributors. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -``` - -## Inter (Font) - -Source: https://github.com/rsms/inter/ - -``` -Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION AND CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. -``` - -## Material Design Icons - -Source: https://github.com/google/material-design-icons -Files: -- `./app/static/material-icons/*` -- `./data/profilepictures/default.svg` - -``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright [yyyy] [name of copyright owner] - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -``` diff --git a/app/admin.py b/app/admin.py index 4513ff0..0d37821 100644 --- a/app/admin.py +++ b/app/admin.py @@ -43,24 +43,20 @@ class CustomUserAdmin(UserAdmin): fieldsets_ = list((*UserAdmin.fieldsets,)) fieldsets_.insert(1, ( - "Visibility", - {"fields": ("hide_from_userlist",)}, - )) - fieldsets_.insert(2, ( "Balance", {"fields": ("balance", "allow_order_with_negative_balance")}, )) - fieldsets_.insert(3, ( + fieldsets_.insert(2, ( "Supply", {"fields": ("allowed_to_supply",)}, )) - fieldsets_.insert(4, ( + fieldsets_.insert(3, ( "Profile Picture", {"fields": ("profile_picture_filename",)}, )) fieldsets = tuple(fieldsets_) - list_display = ["username", "balance", "allow_order_with_negative_balance", "is_active", "hide_from_userlist"] + 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) @@ -95,7 +91,9 @@ class CustomRegisterAdmin(admin.ModelAdmin): 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.") @@ -119,7 +117,9 @@ class CustomOrderAdmin(admin.ModelAdmin): 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" diff --git a/app/db_queries.py b/app/db_queries.py index dd79bbe..93a2727 100644 --- a/app/db_queries.py +++ b/app/db_queries.py @@ -2,8 +2,6 @@ from django.conf import settings from django.db import connection -from django.utils.translation import gettext -from calendar import day_name COMBINE_ALPHABET = "abcdefghijklmnopqrstuvwxyz" @@ -16,6 +14,7 @@ def _db_select(sql_select:str): result = cursor.fetchall() return result + def _combine_results(results:list) -> dict: ''' e.g. @@ -45,42 +44,33 @@ def select_history(user, language_code="en") -> list: user_id = user.pk result = _db_select(f""" select - price_sum as "sum", concat( - product_name, - ' (', - content_litres::real, -- converting to real removes trailing zeros - 'l) x ', amount - ) as "text", + 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 + union select - transaction_sum as "sum", - '{gettext("Deposit")}' as "text", + concat('Deposit: +', transaction_sum, '{settings.CURRENCY_SUFFIX}') as "text", datetime from app_userdeposits_view where user_id = {user_id} - union - - select - transaction_sum as "sum", - comment as "text", - datetime - from app_registertransaction - where user_id = {user_id} and is_transfer = true - 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_orders_per_month(user) -> dict: + +def orders_per_month(user) -> list: # number of orders per month (last 12 months) result_user = _db_select(f""" select @@ -101,35 +91,32 @@ def select_orders_per_month(user) -> dict: group by "month" order by "month" desc; """) - return _combine_results([result_all, result_user]) + return _combine_results([result_user, result_all]) -def select_orders_per_weekday(user) -> list: + +def orders_per_weekday(user) -> list: # number of orders per weekday (all time) - result = _db_select(f""" - with q_all as ( - select - extract(isodow from datetime) as "d", - sum(amount) as "c" - from app_order - group by d - ), q_user as ( - select - extract(isodow from datetime) as "d", - sum(amount) as "c" - from app_order - where user_id = {user.pk} - group by d - ) - select q_all.d as "day", q_all.c, q_user.c from q_all full join q_user on q_all.d = q_user.d - group by day, q_all.c, q_user.c - order by day asc; + result_user = _db_select(f""" + select + to_char(datetime, 'Day') as "day", + sum(amount) as "count" + from app_order + where user_id = {user.pk} + group by "day" + order by "count" desc; """) - for i in range(len(result)): - day_, all_, user_ = result[i] - result[i] = (day_name[int(day_)-1], all_, user_) - return result + result_all = _db_select(f""" + select + to_char(datetime, 'Day') as "day", + sum(amount) as "count" + from app_order + group by "day" + order by "count" desc; + """) + return _combine_results([result_user, result_all]) -def select_orders_per_drink(user) -> dict: + +def orders_per_drink(user) -> list: # number of orders per drink (all time) result_user = _db_select(f""" select @@ -150,31 +137,4 @@ def select_orders_per_drink(user) -> dict: group by d.product_name order by "data" desc; """) - return _combine_results([result_all, result_user]) - -def select_order_sum_per_user_all_users() -> list: - # sum of all orders per user, for all users - result = _db_select(f""" - select - app_user.username as user, - sum(app_order.price_sum) as sum - from app_user - left outer join app_order on (app_user.id = app_order.user_id) - group by app_user.id - order by app_user asc; - """) - return result - -def select_deposit_sum_per_user_all_users() -> list: - # sum of all orders per user, for all users - result = _db_select(f""" - select - app_user.username as user, - sum(rt.transaction_sum) as sum - from app_user - left outer join app_registertransaction rt on (app_user.id = rt.user_id) - where rt.is_user_deposit is true or rt.is_user_deposit is null - group by app_user.id - order by app_user asc; - """) - return result + return _combine_results([result_user, result_all]) diff --git a/app/locales/de/LC_MESSAGES/django.mo b/app/locales/de/LC_MESSAGES/django.mo index 5d60623..76e345b 100644 Binary files a/app/locales/de/LC_MESSAGES/django.mo and b/app/locales/de/LC_MESSAGES/django.mo differ diff --git a/app/locales/de/LC_MESSAGES/django.po b/app/locales/de/LC_MESSAGES/django.po index 278ff86..c7b1011 100644 --- a/app/locales/de/LC_MESSAGES/django.po +++ b/app/locales/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-11-01 19:29+0100\n" +"POT-Creation-Date: 2023-02-17 22:11+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Julian Müller (ChaoticByte)\n" "Language: DE\n" @@ -33,21 +33,20 @@ msgstr "Ein Fehler ist aufgetreten. Bitte ab- und wieder anmelden." msgid "Drinks - Deposit" msgstr "Getränke - Einzahlen" -#: app/templates/deposit.html:17 app/templates/userpanel.html:18 +#: app/templates/deposit.html:17 app/templates/userpanel.html:23 msgid "Deposit" msgstr "Einzahlen" -#: app/templates/deposit.html:19 app/templates/transfer.html:43 +#: app/templates/deposit.html:19 msgid "Amount" msgstr "Summe" #: app/templates/deposit.html:30 app/templates/order.html:54 #: app/templates/registration/login.html:28 app/templates/supply.html:29 -#: app/templates/transfer.html:54 msgid "cancel" msgstr "Abbrechen" -#: app/templates/deposit.html:31 app/templates/transfer.html:55 +#: app/templates/deposit.html:31 msgid "confirm" msgstr "Bestätigen" @@ -55,7 +54,7 @@ msgstr "Bestätigen" msgid "Drinks - History" msgstr "Getränke - Verlauf" -#: app/templates/history.html:10 app/templates/userpanel.html:23 +#: app/templates/history.html:10 app/templates/userpanel.html:30 msgid "History" msgstr "Verlauf" @@ -165,65 +164,49 @@ msgstr "Wähle deinen Account" msgid "Drinks - Statistics" msgstr "Getränke - Statistiken" -#: app/templates/statistics.html:10 app/templates/userpanel.html:24 +#: app/templates/statistics.html:10 app/templates/userpanel.html:31 msgid "Statistics" msgstr "Statistiken" #: app/templates/statistics.html:13 -msgid "orders / drink" -msgstr "Bestellungen / Getränk" +msgid "Orders per drink" +msgstr "Bestellungen pro Getränk" #: app/templates/statistics.html:16 msgid "drink" msgstr "Getränk" -#: app/templates/statistics.html:17 app/templates/statistics.html:36 -#: app/templates/statistics.html:53 -msgid "all" -msgstr "Alle" - -#: app/templates/statistics.html:18 app/templates/statistics.html:37 -#: app/templates/statistics.html:54 +#: app/templates/statistics.html:17 app/templates/statistics.html:34 +#: app/templates/statistics.html:51 msgid "you" msgstr "Du" -#: app/templates/statistics.html:32 -msgid "orders / month" -msgstr "Bestellungen / Monat" +#: app/templates/statistics.html:18 app/templates/statistics.html:35 +#: app/templates/statistics.html:52 +msgid "all" +msgstr "Alle" -#: app/templates/statistics.html:35 +#: app/templates/statistics.html:30 +msgid "Orders per month (last 12 months)" +msgstr "Bestellungen pro Monat (letzte 12 Monate)" + +#: app/templates/statistics.html:33 msgid "month" msgstr "Monat" -#: app/templates/statistics.html:49 -msgid "orders / weekday" -msgstr "Bestellungen / Wochentag" +#: app/templates/statistics.html:47 +msgid "Orders per weekday" +msgstr "Bestellungen pro Wochentag" -#: app/templates/statistics.html:52 +#: app/templates/statistics.html:50 msgid "day" msgstr "Tag" -#: app/templates/statistics.html:69 -msgid "order sum" -msgstr "Bestellungen" - -#: app/templates/statistics.html:72 app/templates/statistics.html:89 -msgid "user" -msgstr "Benutzer" - -#: app/templates/statistics.html:73 app/templates/statistics.html:90 -msgid "sum" -msgstr "Summe" - -#: app/templates/statistics.html:86 -msgid "deposit sum" -msgstr "Einzahlungen" - #: app/templates/supply.html:7 msgid "Drinks - Supply" msgstr "Getränke - Beschaffung" -#: app/templates/supply.html:14 app/templates/userpanel.html:30 +#: app/templates/supply.html:14 app/templates/userpanel.html:36 msgid "Supply" msgstr "Beschaffung" @@ -243,31 +226,23 @@ msgstr "Senden" msgid "You are not allowed to view this site." msgstr "Dir fehlt die Berechtigung, diese Seite anzuzeigen." -#: app/templates/transfer.html:6 -msgid "Drinks - Transfer" -msgstr "Getränke - Geld senden" +#: app/templates/userpanel.html:9 app/templates/userpanel.html:11 +msgid "User" +msgstr "Benutzer" -#: app/templates/transfer.html:17 -msgid "Transfer Money" -msgstr "Geld senden" - -#: app/templates/transfer.html:19 -msgid "Recipient" -msgstr "Empfänger" - -#: app/templates/userpanel.html:10 app/templates/userpanel.html:12 +#: app/templates/userpanel.html:15 app/templates/userpanel.html:17 msgid "Balance" msgstr "Saldo" -#: app/templates/userpanel.html:19 +#: app/templates/userpanel.html:24 msgid "Logout" msgstr "Abmelden" -#: app/templates/userpanel.html:28 -msgid "Transfer" -msgstr "Geld senden" +#: app/templates/userpanel.html:27 +msgid "Account" +msgstr "Account" -#: app/templates/userpanel.html:32 +#: app/templates/userpanel.html:38 msgid "Change Password" msgstr "Passwort ändern" diff --git a/app/migrations/0003_user_hide_from_userlist.py b/app/migrations/0003_user_hide_from_userlist.py deleted file mode 100644 index 7ffa0e8..0000000 --- a/app/migrations/0003_user_hide_from_userlist.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 4.1.6 on 2023-04-13 19:12 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("app", "0002_setup"), - ] - - operations = [ - migrations.AddField( - model_name="user", - name="hide_from_userlist", - field=models.BooleanField(default=False), - ), - ] diff --git a/app/migrations/0004_registertransaction_is_transfer.py b/app/migrations/0004_registertransaction_is_transfer.py deleted file mode 100644 index 550b353..0000000 --- a/app/migrations/0004_registertransaction_is_transfer.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 4.1.6 on 2023-04-14 20:01 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("app", "0003_user_hide_from_userlist"), - ] - - operations = [ - migrations.AddField( - model_name="registertransaction", - name="is_transfer", - field=models.BooleanField(default=False), - ), - ] diff --git a/app/models.py b/app/models.py index 717a1cf..ddfd302 100644 --- a/app/models.py +++ b/app/models.py @@ -13,7 +13,6 @@ class User(AbstractUser): allow_order_with_negative_balance = models.BooleanField(default=False) profile_picture_filename = models.CharField(default="default.svg", max_length=25) allowed_to_supply = models.BooleanField(default=False) - hide_from_userlist = models.BooleanField(default=False) def delete(self, *args, **kwargs): self.balance = 0 @@ -40,12 +39,11 @@ class Drink(models.Model): do_not_count = models.BooleanField(default=False) def delete(self, *args, **kwargs): - # we flag the field as deleted. self.deleted = True super().save() def __str__(self): - return f"{self.product_name} ({float(self.content_litres):.2f}l) - {self.price} {settings.CURRENCY_SUFFIX}" + return f"{self.product_name} ({float(self.content_litres):.2f}l) - {self.price}{settings.CURRENCY_SUFFIX}" class RegisterTransaction(models.Model): @@ -60,34 +58,35 @@ class RegisterTransaction(models.Model): 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) - is_transfer = models.BooleanField(default=False) comment = models.TextField(default=" ") user = models.ForeignKey(User, on_delete=models.CASCADE) def save(self, *args, **kwargs): if self._state.adding: - if self.is_user_deposit or self.is_transfer: # update user balance + 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 user balance - if self.is_user_deposit or self.is_transfer: - self.user.balance += self.transaction_sum - self.old_transaction_sum - self.user.save() # update register transaction + sum_diff = self.transaction_sum - self.old_transaction_sum + # 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): # update user deposit - if self.is_user_deposit or self.is_transfer: + 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}" + def __str__(self): return f"{self.transaction_sum}{settings.CURRENCY_SUFFIX} by {self.user}" class Order(models.Model): @@ -108,9 +107,10 @@ class Order(models.Model): 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): - # saving this may affect other fields - # so we reimplement the save function drink = Drink.objects.get(pk=self.drink.pk) if self._state.adding and drink.available > 0: if not drink.do_not_count: @@ -126,7 +126,6 @@ class Order(models.Model): raise ValidationError("This entry can't be changed.") def delete(self, *args, **kwargs): - # when deleting, we affect other fields as well. self.user.balance += self.price_sum self.user.save() drink = Drink.objects.get(pk=self.drink.pk) @@ -135,7 +134,7 @@ class Order(models.Model): drink.save() super().delete(*args, **kwargs) - def __str__(self): return f"{self.drink.product_name} ({float(self.drink.content_litres):.2f}l) x {self.amount} - {self.price_sum} {settings.CURRENCY_SUFFIX}" + def __str__(self): return f"{self.drink.product_name} ({float(self.drink.content_litres):.2f}l) x {self.amount} - {self.price_sum}{settings.CURRENCY_SUFFIX}" class Global(models.Model): diff --git a/static/css/main.css b/app/static/css/main.css similarity index 58% rename from static/css/main.css rename to app/static/css/main.css index ea44432..afdd6c6 100644 --- a/static/css/main.css +++ b/app/static/css/main.css @@ -1,56 +1,28 @@ -/* Fonts */ - -@font-face { - font-family: "Inter"; - src: url('/static/fonts/Inter-Regular.ttf'); - font-weight: normal; - font-style: normal; -} -@font-face { - font-family: "Inter"; - src: url('/static/fonts/Inter-Bold.ttf'); - font-weight: bold; - font-style: normal; -} - /* Variables */ :root { - --font-family: "Inter"; + --font-family: 'Liberation Sans', sans-serif; --color: #fafafa; - --color-disabled: #ffffff50; - --color-error: #ff817c; - --bg-page: linear-gradient( - -10deg, - #071c29 10%, - #4a8897 - ); - --bg-color: #ffffff35; - --bg-color2: #ffffff25; - --bg-hover-color: #ffffff50; - --border-color: #ffffff50; + --color-error: #ff682c; + --bg-page-color: #222222; + --bg-color: #4e4e4e; + --bg-hover-color: #636363; + --bg-color2: #383838; + --bg-hover-color2: #4a4a4a; + --border-color: #808080; --bg-globalmessage: #161616; - --border-radius: .6rem; - --element-padding: .6rem .8rem; + --border-radius: .5rem; } /* General */ -body, -input, -select, -button, .button -{ - font-family: var(--font-family); -} - body { margin: 0; padding: 0; width: 100vw; min-height: 100vh; - font-size: 17px; - background: var(--bg-page); + font-family: var(--font-family); + background: var(--bg-page-color); color: var(--color); overflow-x: hidden; } @@ -60,7 +32,7 @@ a { } h1 { - font-size: 28px; + font-size: 1.8rem; } h1, h2, h3, h4 { @@ -78,38 +50,18 @@ input[type="number"]::-webkit-inner-spin-button { display: none; } -input[type="text"], -input[type="password"], -input[type="number"], -select { - padding: var(--element-padding); - text-align: center !important; - font-size: 16px; +input[type="text"], input[type="password"], input[type="number"] { + padding: .6rem .8rem; + text-align: center; + font-size: 1rem; color: var(--color); border: none; outline: none; - border: 1px solid var(--border-color); + border-bottom: 1px solid var(--border-color); border-radius: var(--border-radius); background: var(--bg-color); } -input[type="text"]::placeholder, -input[type="password"]::placeholder, -input[type="number"]::placeholder, -select > option:disabled { - color: var(--color-disabled); -} - -select { - appearance: none; - -webkit-appearance: none; - -moz-appearance: none; - background-image: url("/static/material-icons/arrow-drop-down.svg"); - background-repeat: no-repeat; - background-position: right; - background-size: 1.5rem; -} - table { border-collapse: collapse; border-spacing: 0; @@ -117,15 +69,20 @@ table { border-radius: var(--border-radius); } -tr > th, -tr > td { +tr { background: var(--bg-color); } -tr:nth-child(2n+2) > td { +tr:nth-child(2n+2) { background: var(--bg-color2); } +/* +Rounded corners on table cells apparently don't work with +Firefox, so Firefox users won't have rounded corners +on tables. Can't fix that by myself. +*/ + table tr:first-child th:first-child { border-top-left-radius: var(--border-radius); } @@ -170,6 +127,7 @@ th { .globalmessage > div { width: 96%; + text-align: center; word-break: keep-all; word-wrap: break-word; box-sizing: border-box; @@ -179,7 +137,11 @@ th { flex-direction: row; margin-top: 1rem; width: 94%; - gap: 2rem; + gap: 1rem; +} + +.userinfo { + text-align: center; } .userinfo > span { @@ -197,10 +159,6 @@ th { gap: .5rem; } -.userpanel-buttons > .button, .userpanel-buttons button { - height: 1.2rem; -} - .userbalancewarn { color: var(--color-error); font-weight: bold; @@ -234,7 +192,7 @@ main { } .footer > div { - font-size: 16px; + font-size: .95rem; margin-top: .15rem; margin-bottom: .15rem; } @@ -274,18 +232,19 @@ main { flex-wrap: wrap; } -.text-align-right { - text-align: right; -} - -.text-align-center { - text-align: center; -} - .gap-1rem { gap: 1rem; } +.fill { + height: 100%; + width: 100%; +} + +.fill-vertical { + height: 100%; +} + .buttons { display: flex; flex-direction: row; @@ -298,26 +257,24 @@ main { display: flex; align-items: center; justify-content: center; + font-family: var(--font-family); + text-decoration: none; + text-align: center !important; + background: var(--bg-color); + color: var(--color); + font-size: 1rem; + padding: .6rem .8rem; outline: none; - border: 1px solid var(--border-color); + border: none; + border-bottom: 1px solid var(--border-color); border-radius: var(--border-radius); + cursor: pointer; + user-select: none; + box-sizing: content-box; width: fit-content; } -.button, button, .dropdownchoice { - padding: var(--element-padding); - font-size: 16px; - text-align: center !important; - text-decoration: none; - color: var(--color); - box-sizing: content-box; - cursor: pointer; - user-select: none; - background: var(--bg-color); -} - -.button:hover, button:hover, -.button:active, button:active { +.button:hover, button:hover, .button:active, button:active { background: var(--bg-hover-color); } @@ -325,55 +282,30 @@ main { opacity: 40%; } -.formheading { - margin-bottom: 2rem; -} - -.forminfo { - width: fit-content; - min-width: 16rem; +.appform > .forminfo { + width: 100%; text-align: left; display: flex; flex-direction: row; justify-content: space-between; gap: 2rem; - padding-bottom: .15rem; - border-bottom: 1px solid #ffffff20; } .forminfo > span:last-child { float: right; } -.appform, .appform > * { - max-width: 90vw; -} - .appform > .forminput { width: 100%; flex-direction: row; - justify-content: space-between; + justify-content: space-evenly; align-items: center; flex-wrap: wrap; gap: 1rem; } -.forminput > input, .forminput > select { - width: 100% !important; -} - -.forminput > .keyboard-input, #transfer-recipient { - /* the keyboard has a 5px padding */ - margin-left: 5px !important; - margin-right: 5px !important; -} - -.appform > .buttons { - margin-top: 1rem; -} - -#statusinfo { - margin-top: 1rem; +.appform > .statusinfo { + margin-top: .5rem; } .dropdownmenu { @@ -384,89 +316,77 @@ main { border-radius: var(--border-radius); } -#dropdownnope { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - margin: 0; - padding: 0; -} - .dropdownbutton { + width: fit-content; z-index: 190; -} - -.dropdownbutton > img { - width: auto; - height: 100%; + text-align: center; + justify-content: center; } .dropdownlist { - margin-top: 3rem; position: absolute; display: flex; flex-direction: column; - border-radius: var(--border-radius); - box-shadow: 0 0 .5rem #00000025; -} - -.dropdownlist, #dropdownnope { - visibility: hidden; - opacity: 0%; pointer-events: none; -} - -.dropdownvisible .dropdownlist, -.dropdownvisible #dropdownnope { - opacity: 100%; - background: #00000020; - visibility: visible; - pointer-events: visible; - z-index: 100; + border-radius: var(--border-radius) !important; + z-index: 200; + margin-top: 3.2rem; + opacity: 0%; + transition: opacity 100ms; } .dropdownchoice { - z-index: 200; + border-radius: 0 !important; margin: 0; - text-decoration: none; + text-align: center; + justify-content: center; + background: var(--bg-color2) !important; + backdrop-filter: none !important; width: initial; - min-width: max-content; - border-bottom: 1px solid var(--border-color); - border-left: 1px solid var(--border-color); - border-right: 1px solid var(--border-color); -} - -.dropdownchoice:first-child { - border-top: 1px solid var(--border-color); - border-top-left-radius: var(--border-radius); - border-top-right-radius: var(--border-radius); -} - -.dropdownchoice:last-child { - border-bottom: 1px solid var(--border-color); - border-bottom-left-radius: var(--border-radius); - border-bottom-right-radius: var(--border-radius); } .dropdownchoice:hover { - background: var(--bg-hover-color); + background: var(--bg-hover-color2) !important; +} + +.dropdownlist :first-child { + border-top-left-radius: var(--border-radius) !important; + border-top-right-radius: var(--border-radius) !important; +} + +.dropdownlist :last-child { + border-bottom-left-radius: var(--border-radius) !important; + border-bottom-right-radius: var(--border-radius) !important; +} + +.dropdownvisible .dropdownlist { + opacity: 100%; + visibility: visible; + pointer-events: visible; } .customnumberinput { height: 2.2rem; - display: flex; - flex-direction: row; - align-items: center; - gap: .25rem; } .customnumberinput button { - width: 2.2rem !important; - height: 2.2rem !important; + min-width: 2.5rem !important; + width: 2.5rem !important; padding: 0; margin: 0; + height: 100%; +} + +.customnumberinput-minus { + border-bottom-right-radius: 0; + border-top-right-radius: 0; + z-index: 10; +} + +.customnumberinput-plus { + border-bottom-left-radius: 0; + border-top-left-radius: 0; + z-index: 10; } .customnumberinput input[type="number"] { @@ -475,13 +395,13 @@ main { padding: 0; margin: 0; background: var(--bg-color2); + border-radius: 0 !important; -webkit-appearance: textfield; -moz-appearance: textfield; appearance: textfield; } .errortext { - font-weight: bold; color: var(--color-error); } @@ -491,11 +411,6 @@ main { /* Login */ -.userlist-container { - flex-grow: 1; - padding-bottom: 10vh; -} - .userlist { width: 60%; list-style: none; @@ -505,7 +420,8 @@ main { } .userlist > li { - padding: .1rem .6rem; + margin-bottom: .5rem; + padding: 0 .5rem; } .userlist > li > img { @@ -518,7 +434,7 @@ main { .userlist > li > div { flex-grow: 1; text-align: center; - padding: .7rem 1.1rem; + padding: .8rem 1.1rem; } .loginform { @@ -530,18 +446,6 @@ main { margin-top: 0; } -#passwordoverlay-container { - position: fixed; - width: 100vw; - height: 100vh; - top: 0; - right: 0; - background: var(--bg-page); - align-items: center; - padding-top: 10vh; - z-index: 200; -} - /* Drinks List */ .drinks-list { @@ -558,37 +462,7 @@ main { .drinks-list > li > .button { width: 100%; justify-content: space-between; - padding: .7rem 1.1rem; -} - -/* Statistics */ - -.statistics-container { - display: flex; - flex-direction: row; - align-items: flex-start; - justify-content: center; - flex-wrap: wrap; - max-width: 90vw; - gap: 1rem; -} - -.statistics-container > div { - height: 100%; - width: 16rem; -} - -/* Blur */ - -@supports (backdrop-filter: blur()) { - .dropdownvisible #dropdownnope { - backdrop-filter: blur(16px); - } - #passwordoverlay-container { - background: #00000020; - backdrop-filter: blur(64px); /* fallback */ - backdrop-filter: blur(128px); - } + padding: .8rem 1.1rem; } /* Responsive */ @@ -611,10 +485,9 @@ main { } } -@media only screen and (max-width: 860px) { +@media only screen and (max-width: 700px) { .userpanel { flex-direction: column; - gap: 1rem; } .userlist { gap: 0.25rem; @@ -631,12 +504,4 @@ main { .drinks-list { width: 90%; } - .dropdownlist { - width: 14rem; - right: calc(50vw - 7rem); /* regard width */ - left: auto; - } - #keyboard { - display: none !important; - } } diff --git a/static/css/simple-keyboard.css b/app/static/css/simple-keyboard.css similarity index 100% rename from static/css/simple-keyboard.css rename to app/static/css/simple-keyboard.css diff --git a/static/css/simple-keyboard_custom.css b/app/static/css/simple-keyboard_dark.css similarity index 78% rename from static/css/simple-keyboard_custom.css rename to app/static/css/simple-keyboard_dark.css index 39da287..16bfec5 100644 --- a/static/css/simple-keyboard_custom.css +++ b/app/static/css/simple-keyboard_dark.css @@ -5,8 +5,6 @@ width: 50rem; max-width: 100%; background: transparent; - font-family: "Inter"; - font-size: 16px; } .simple-keyboard.darkTheme .hg-button { height: 50px; @@ -15,8 +13,8 @@ align-items: center; background: var(--bg-color); color: white; - border: 1px solid var(--border-color); - border-radius: var(--border-radius); + border: none; + border-bottom: 1px solid var(--border-color); } .simple-keyboard.darkTheme .hg-button:active, .simple-keyboard.darkTheme .hg-button:hover { diff --git a/static/favicon.ico b/app/static/favicon.ico similarity index 100% rename from static/favicon.ico rename to app/static/favicon.ico diff --git a/static/favicon.png b/app/static/favicon.png similarity index 100% rename from static/favicon.png rename to app/static/favicon.png diff --git a/static/js/autoreload.js b/app/static/js/autoreload.js similarity index 100% rename from static/js/autoreload.js rename to app/static/js/autoreload.js diff --git a/static/js/custom_number_input.js b/app/static/js/custom_number_input.js similarity index 100% rename from static/js/custom_number_input.js rename to app/static/js/custom_number_input.js diff --git a/static/js/custom_form.js b/app/static/js/deposit.js similarity index 78% rename from static/js/custom_form.js rename to app/static/js/deposit.js index a29f07b..c2b6785 100644 --- a/static/js/custom_form.js +++ b/app/static/js/deposit.js @@ -1,15 +1,15 @@ document.addEventListener("DOMContentLoaded", () => { // elements - let customForm = document.getElementById("customform"); + let depositForm = document.getElementById("depositform"); let statusInfo = document.getElementById("statusinfo"); - let submitButton = document.getElementById("submitbtn"); + let depositSubmitButton = document.getElementById("depositsubmitbtn"); // event listener for deposit form // this implements a custom submit method - customForm.addEventListener("submit", (event) => { - submitButton.disabled = true; + depositForm.addEventListener("submit", (event) => { + depositSubmitButton.disabled = true; event.preventDefault(); // Don't do the default submit action! let xhr = new XMLHttpRequest(); - let formData = new FormData(customForm); + let formData = new FormData(depositForm); xhr.addEventListener("load", (event) => { status_ = event.target.status; response_ = event.target.responseText; @@ -28,7 +28,7 @@ document.addEventListener("DOMContentLoaded", () => { statusInfo.innerText = "An error occured. Redirecting in 5 seconds..."; window.setTimeout(() => { window.location.replace("/") }, 5000); }) - xhr.open("POST", customForm.action); + xhr.open("POST", "/api/deposit"); xhr.send(formData); }); }); diff --git a/static/js/logged_out.js b/app/static/js/logged_out.js similarity index 100% rename from static/js/logged_out.js rename to app/static/js/logged_out.js diff --git a/static/js/login.js b/app/static/js/login.js similarity index 85% rename from static/js/login.js rename to app/static/js/login.js index 6f62216..0c43a72 100644 --- a/static/js/login.js +++ b/app/static/js/login.js @@ -6,6 +6,7 @@ let passwordOverlayElement; let pwOverlayCancelButton; let userlistButtons; + let pinpadButtons; let userlistContainerElement; // Add event listeners after DOM Content loaded document.addEventListener("DOMContentLoaded", () => { @@ -35,14 +36,9 @@ function show_password_overlay() { window.scrollTo(0, 0); passwordOverlayElement.classList.remove("nodisplay"); - passwordInputElement.focus() } function hide_password_overlay() { passwordOverlayElement.classList.add("nodisplay"); passwordInputElement.value = ""; - // Dispatch an Input Event to the input element to trigger the on- - // screen keyboard to update its buffer. This fixes a security - // issue on the login page. - passwordInputElement.dispatchEvent(new Event("input", {bubbles: true})); } })(); diff --git a/app/static/js/main.js b/app/static/js/main.js new file mode 100644 index 0000000..711aed8 --- /dev/null +++ b/app/static/js/main.js @@ -0,0 +1,14 @@ +document.addEventListener("DOMContentLoaded", () => { + let dropdownmenuElement = document.getElementById("dropdownmenu"); + let dropdownmenuButtonElement = document.getElementById("dropdownmenu-button"); + if (dropdownmenuButtonElement != null) { + dropdownmenuButtonElement.addEventListener("click", () => { + if (dropdownmenuElement.classList.contains("dropdownvisible")) { + dropdownmenuElement.classList.remove("dropdownvisible"); + } + else { + dropdownmenuElement.classList.add("dropdownvisible"); + } + }) + } +}); diff --git a/static/js/order.js b/app/static/js/order.js similarity index 100% rename from static/js/order.js rename to app/static/js/order.js diff --git a/static/js/simple-keyboard.js b/app/static/js/simple-keyboard.js similarity index 100% rename from static/js/simple-keyboard.js rename to app/static/js/simple-keyboard.js diff --git a/static/js/simple-keyboard_configure.js b/app/static/js/simple-keyboard_configure.js similarity index 98% rename from static/js/simple-keyboard_configure.js rename to app/static/js/simple-keyboard_configure.js index 8aa9aec..326c68e 100644 --- a/static/js/simple-keyboard_configure.js +++ b/app/static/js/simple-keyboard_configure.js @@ -38,8 +38,7 @@ "1 2 3", "4 5 6", "7 8 9", - "0 . ,", - "{bksp}" + "{bksp} . ," ] } // Check if on smartphone diff --git a/app/static/js/supply.js b/app/static/js/supply.js new file mode 100644 index 0000000..04021e0 --- /dev/null +++ b/app/static/js/supply.js @@ -0,0 +1,39 @@ +document.addEventListener("DOMContentLoaded", () => { + // elements + let supplyDescriptionElement = document.getElementById("supplydescription"); + let supplyPriceElement = document.getElementById("supplyprice"); + let supplyFormElement = document.getElementById("supplyform"); + let statusInfoElement = document.getElementById("statusinfo"); + let supplySubmitButton = document.getElementById("supplysubmitbtn"); + // custom submit method + supplyFormElement.addEventListener("submit", (event) => { + supplySubmitButton.disabled = true; + event.preventDefault(); // Don't do the default submit action! + if (isNaN(parseFloat(supplyPriceElement.value)) || supplyDescriptionElement.value == "") { + statusInfoElement.innerText = "Please enter a description and price." + supplySubmitButton.disabled = false; + } + let xhr = new XMLHttpRequest(); + let formData = new FormData(supplyFormElement); + xhr.addEventListener("load", (event) => { + status_ = event.target.status; + response_ = event.target.responseText; + if (status_ == 200 && response_ == "success") { + statusInfoElement.innerText = "Success."; + window.location.replace("/"); + } + else { + statusInfoElement.classList.add("errortext"); + statusInfoElement.innerText = "An error occured."; + window.setTimeout(() => { window.location.reload() }, 5000); + } + }) + xhr.addEventListener("error", (event) => { + statusInfoElement.classList.add("errortext"); + statusInfoElement.innerText = "An error occured."; + window.setTimeout(() => { window.location.reload() }, 5000); + }) + xhr.open("POST", "/api/supply"); + xhr.send(formData); + }); +}); diff --git a/app/templates/baselayout.html b/app/templates/baselayout.html index 97ac907..5783cf9 100644 --- a/app/templates/baselayout.html +++ b/app/templates/baselayout.html @@ -25,7 +25,7 @@
{% translate "An error occured. Please log out and log in again." %}
- log out + log out
{% endif %} diff --git a/app/templates/deposit.html b/app/templates/deposit.html index cf3dbc1..9334b3c 100644 --- a/app/templates/deposit.html +++ b/app/templates/deposit.html @@ -8,26 +8,29 @@ {% block headAdditional %} - + {% endblock %} {% block content %} -

{% translate "Deposit" %}

-
+ {% csrf_token %} +

{% translate "Deposit" %}

- + {% translate "Amount" %} {{ currency_suffix }}: + + +
+
{% translate "cancel" %} - +
-
- + {% endblock %} \ No newline at end of file diff --git a/app/templates/footer.html b/app/templates/footer.html index 63b51ec..89d58c6 100644 --- a/app/templates/footer.html +++ b/app/templates/footer.html @@ -2,6 +2,6 @@ \ No newline at end of file diff --git a/app/templates/globalmessage.html b/app/templates/globalmessage.html index 4ec0067..9b33850 100644 --- a/app/templates/globalmessage.html +++ b/app/templates/globalmessage.html @@ -1,5 +1,5 @@ {% if global_message != "" %} -
+
{{ global_message }}
{% endif %} \ No newline at end of file diff --git a/app/templates/history.html b/app/templates/history.html index c94f17e..89d689a 100644 --- a/app/templates/history.html +++ b/app/templates/history.html @@ -11,13 +11,13 @@ {% if history %} - + + {% for h in history %} - - - + + {% endfor %}
{% translate "last 30 actions" %}{% translate "last 30 actions" %}
{{ h.0 }} {{ currency_suffix }}{{ h.1 }}{{ h.2 }}{{ h.0 }}{{ h.1 }}
diff --git a/app/templates/order.html b/app/templates/order.html index 2e5286f..2cae204 100644 --- a/app/templates/order.html +++ b/app/templates/order.html @@ -11,31 +11,31 @@
{% if drink and drink.available > 0 and not drink.deleted %} {% if user.balance > 0 or user.allow_order_with_negative_balance %} -

{% translate "Order" %}

{% csrf_token %} +

{% translate "Order" %}

- {% translate "Drink" %} + {% translate "Drink" %}: {{ drink.product_name }}
- {% translate "Price per Item" %} ({{ currency_suffix }}) + {% translate "Price per Item" %} ({{ currency_suffix }}): {{ drink.price }}
{% if not drink.do_not_count %}
- {% translate "Available" %} + {% translate "Available" %}: {{ drink.available }}
{% endif %}
- {% translate "Sum" %} ({{ currency_suffix }}) + {% translate "Sum" %} ({{ currency_suffix }}): {{ drink.price }}
- {% translate "Count" %} + {% translate "Count" %}: {% if drink.do_not_count %} @@ -48,19 +48,19 @@
+
-
{% else %} -
+

{% translate "Your balance is too low to order a drink." %}

- {% translate "back" %} + {% translate "back" %}
{% endif %} {% else %} diff --git a/app/templates/registration/login.html b/app/templates/registration/login.html index 657c713..837dc70 100644 --- a/app/templates/registration/login.html +++ b/app/templates/registration/login.html @@ -10,7 +10,7 @@ {% block headAdditional %} - + {% endblock %} {% block content %} @@ -19,7 +19,7 @@ {% endif %}
-

{% translate "Log in" %}

+

{% translate "Log in" %}

{% csrf_token %} @@ -36,7 +36,7 @@
-
+

{% translate "Choose your account" %}

    {% for user_ in user_list %} @@ -48,8 +48,6 @@ {{ user_.last_name }}, {% endif %} {{ user_.first_name }} - {% elif user_.last_name %} - {{ user_.last_name }} {% else %} {{ user_.username }} {% endif %} diff --git a/app/templates/statistics.html b/app/templates/statistics.html index 93f3a9e..4f938b9 100644 --- a/app/templates/statistics.html +++ b/app/templates/statistics.html @@ -8,96 +8,57 @@ {% block content %}

    {% translate "Statistics" %}

    -
    -
    -

    {% translate "orders / drink" %}

    +
    +
    +

    {% translate "Orders per drink" %}

    - + {% for key, values in orders_per_drink.items %} - - + + {% endfor %}
    {% translate "drink" %}{% translate "all" %} {% translate "you" %}{% translate "all" %}
    {{ key }}{{ values.a|default:0 }}{{ values.b|default:0 }}{{ values.a|default:"0" }}{{ values.b|default:"0" }}
    -
    -
    -
    -

    {% translate "orders / month" %}

    +
    +

    {% translate "Orders per month (last 12 months)" %}

    - + {% for key, values in orders_per_month.items %} - - + + {% endfor %}
    {% translate "month" %}{% translate "all" %} {% translate "you" %}{% translate "all" %}
    {{ key }}{{ values.a|default:0 }}{{ values.b|default:0 }}{{ values.a|default:"0" }}{{ values.b|default:"0" }}
    -
    -

    {% translate "orders / weekday" %}

    +
    +

    {% translate "Orders per weekday" %}

    - + - {% for values in orders_per_weekday %} + {% for key, values in orders_per_weekday.items %} - - - + + + {% endfor %}
    {% translate "day" %}{% translate "all" %} {% translate "you" %}{% translate "all" %}
    {{ values.0 }}{{ values.1|default:0 }}{{ values.2|default:0 }}{{ key }}{{ values.a|default:"0" }}{{ values.b|default:"0" }}
    -
    -
    - {% if user.is_superuser or perms.app.view_order %} -
    -

    {% translate "order sum" %}

    - - - - - - {% for values in order_sum_per_user %} - - - - - {% endfor %} -
    {% translate "user" %}{% translate "sum" %}
    {{ values.0 }}{{ values.1|default:0.0 }} {{ currency_suffix }}
    -
    - {% endif %} - {% if user.is_superuser or perms.app.view_registertransaction %} -
    -

    {% translate "deposit sum" %}

    - - - - - - {% for values in deposit_sum_per_user %} - - - - - {% endfor %} -
    {% translate "user" %}{% translate "sum" %}
    {{ values.0 }}{{ values.1|default:0.0 }} {{ currency_suffix }}
    -
    - {% endif %} -
    {% endblock %} \ No newline at end of file diff --git a/app/templates/supply.html b/app/templates/supply.html index bcbc3d0..f907f2d 100644 --- a/app/templates/supply.html +++ b/app/templates/supply.html @@ -9,22 +9,28 @@ {% block content %} {% if user.is_superuser or user.allowed_to_supply %} -

    {% translate "Supply" %}

    - + {% csrf_token %} +

    {% translate "Supply" %}

    - + {% translate "Description" %}: + + +
    - + {% translate "Price" %} ({{ currency_suffix }}): + + +
    +
    -
    - + {% else %}
    diff --git a/app/templates/transfer.html b/app/templates/transfer.html deleted file mode 100644 index bf0524b..0000000 --- a/app/templates/transfer.html +++ /dev/null @@ -1,54 +0,0 @@ -{% extends "baselayout.html" %} - -{% load i18n %} - -{% block title %} -{% translate "Drinks - Transfer" %} -{% endblock %} - -{% block headAdditional %} - - -{% endblock %} - -{% block content %} -

    {% translate "Transfer Money" %}

    -
    - {% csrf_token %} -
    - -
    -
    - -
    - -
    - - - -
    -
    - - -{% endblock %} \ No newline at end of file diff --git a/app/templates/userpanel.html b/app/templates/userpanel.html index 966f86e..159dcf5 100644 --- a/app/templates/userpanel.html +++ b/app/templates/userpanel.html @@ -2,14 +2,19 @@ {% load static %}
    -
    - {% if user.first_name != "" %} - {{ user.first_name }} {{ user.last_name }} ({{ user.username }}){% else %}{{ user.username }}{% endif %} +
    + + + {% if user.first_name != "" %} + {% translate "User" %}: {{ user.first_name }} {{ user.last_name }} ({{ user.username }}) + {% else %} + {% translate "User" %}: {{ user.username }} + {% endif %}  -  {% if user.balance < 0.01 %} - {% translate "Balance" %}: {{ user.balance }} {{ currency_suffix }} + {% translate "Balance" %}: {{ user.balance }}{{ currency_suffix }} {% else %} - {% translate "Balance" %}: {{ user.balance }} {{ currency_suffix }} + {% translate "Balance" %}: {{ user.balance }}{{ currency_suffix }} {% endif %}
    @@ -18,20 +23,20 @@ {% translate "Deposit" %} {% translate "Logout" %}
    -
    +
    \ No newline at end of file diff --git a/app/urls.py b/app/urls.py index 1fa379d..f6f8cc4 100644 --- a/app/urls.py +++ b/app/urls.py @@ -10,7 +10,6 @@ urlpatterns = [ path('history/', views.history), path('deposit/', views.deposit), path('statistics/', views.statistics), - path('transfer/', views.transfer), path('supply/', views.supply), path('accounts/login/', views.login_page, name="login"), path('accounts/logout/', auth_views.LogoutView.as_view(), name='logout'), @@ -20,6 +19,5 @@ urlpatterns = [ # API # path('api/order-drink', views.api_order_drink), path('api/deposit', views.api_deposit), - path('api/transfer', views.api_transfer), path('api/supply', views.api_supply) ] diff --git a/app/views.py b/app/views.py index ba864a4..7f3ea30 100644 --- a/app/views.py +++ b/app/views.py @@ -25,7 +25,7 @@ from .models import RegisterTransaction def login_page(request): - userlist = get_user_model().objects.filter(hide_from_userlist=False).filter(is_active=True).order_by("username") + 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'] @@ -50,6 +50,7 @@ def login_page(request): "user_list": userlist }) + @login_required def index(request): context = { @@ -57,6 +58,7 @@ def index(request): } return render(request, "index.html", context) + @login_required def history(request): context = { @@ -64,6 +66,7 @@ def history(request): } return render(request, "history.html", context) + @login_required def order(request, drinkid): try: @@ -73,6 +76,7 @@ def order(request, drinkid): except Drink.DoesNotExist: return HttpResponseRedirect("/") + @login_required def deposit(request): return render(request, "deposit.html", {}) @@ -80,32 +84,24 @@ def deposit(request): @login_required def statistics(request): - user = request.user context = { - "orders_per_month": db_queries.select_orders_per_month(user), - "orders_per_weekday": db_queries.select_orders_per_weekday(user), - "orders_per_drink": db_queries.select_orders_per_drink(user), + "orders_per_month": db_queries.orders_per_month(request.user), + "orders_per_weekday": db_queries.orders_per_weekday(request.user), + "orders_per_drink": db_queries.orders_per_drink(request.user), } - # Advanced statistics - if user.has_perm("app.view_order") or user.is_superuser: - context["order_sum_per_user"] = db_queries.select_order_sum_per_user_all_users() - if user.has_perm("app.view_registertransaction") or user.is_superuser: - context["deposit_sum_per_user"] = db_queries.select_deposit_sum_per_user_all_users() return render(request, "statistics.html", context) -@login_required -def transfer(request): - userlist = get_user_model().objects.filter(hide_from_userlist=False).filter(is_active=True).order_by("username") - return render(request, "transfer.html", {"user_list": userlist}) @login_required def supply(request): return render(request, "supply.html") + @login_required def redirect_home(request): return HttpResponseRedirect("/") + # API for XHR requests # @login_required @@ -127,6 +123,7 @@ def api_order_drink(request): 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 @@ -143,35 +140,9 @@ def api_deposit(request): 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 a deposit transaction: User: {user.username} - Exception: {e}", file=sys.stderr) + print(f"An exception occured while processing a transaction: User: {user.username} - Exception: {e}", file=sys.stderr) return HttpResponse(b"", status=500) -@login_required -def api_transfer(request): - # check request -> transfer - user = request.user - try: - recipient = get_user_model().objects.get(id=int(request.POST["recipientuser"])) - if recipient.id == user.id: - raise Exception(f"User {user.username} tried to transfer to themself.") - amount = decimal.Decimal(request.POST["transferamount"]) - if 0.00 < amount <= user.balance: - # create transaction - RegisterTransaction.objects.create( - transaction_sum=-amount, - comment=f"Transfer to {recipient.username}", - is_transfer=True, - user=user) - RegisterTransaction.objects.create( - transaction_sum=amount, - comment=f"Transfer from {user.username}", - is_transfer=True, - user=recipient) - return HttpResponse("success", status=200) - else: raise Exception("Transfer amount too big or small.") - except Exception as e: - print(f"An exception occured while processing a transfer transaction: User: {user.username} - Exception: {e}", file=sys.stderr) - return HttpResponse(b"", status=500) @login_required def api_supply(request): diff --git a/data/Caddyfile b/data/Caddyfile index d790c4e..c5bbc58 100644 --- a/data/Caddyfile +++ b/data/Caddyfile @@ -9,8 +9,7 @@ {$CADDY_HOSTS} { # the tls certificates - # tls {$DATADIR}/tls/server.pem {$DATADIR}/tls/server-key.pem - tls internal + tls {$DATADIR}/tls/server.pem {$DATADIR}/tls/server-key.pem route { # profile pictures file_server /profilepictures/* { @@ -18,11 +17,7 @@ } # static files file_server /static/* { - root {$ROOTDIR} - } - # django static files - file_server /django_static/* { - root {$DATADIR}/django_static/.. + root {$DATADIR}/static/.. } # favicon redir /favicon.ico /static/favicon.ico diff --git a/data/profilepictures/default.svg b/data/profilepictures/default.svg index edc6946..7138ef3 100644 --- a/data/profilepictures/default.svg +++ b/data/profilepictures/default.svg @@ -1 +1,28 @@ - \ No newline at end of file + + + + + + + + + + diff --git a/project/settings.py b/project/settings.py index 56ff5fb..a5362ca 100644 --- a/project/settings.py +++ b/project/settings.py @@ -149,8 +149,8 @@ LOCALE_PATHS = [ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.1/howto/static-files/ -STATIC_URL = "django_static/" -STATIC_ROOT = BASE_DIR / "data" / "django_static" +STATIC_URL = "static/" +STATIC_ROOT = BASE_DIR / "data" / "static" # Default primary key field type # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field diff --git a/requirements.txt b/requirements.txt index ceb35b8..2be275b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -Django~=4.2 -psycopg2~=2.9 -uvicorn[standard]~=0.27 +Django~=4.1 +psycopg2~=2.9.5 +uvicorn~=0.20.0 PyYAML~=6.0 diff --git a/scripts/_bootstrap.py b/scripts/_bootstrap.py index c162c03..9533953 100755 --- a/scripts/_bootstrap.py +++ b/scripts/_bootstrap.py @@ -14,17 +14,6 @@ from time import sleep from yaml import safe_load -banner = r""" ___ _ _ -| \ _ _ (_) _ _ | |__ ___ ___ -| |) || '_|| || ' \ | / /(_-< |___| -|___/ |_| |_||_||_||_\_\/__/ - __ __ Version {version} -| \/ | __ _ _ _ __ _ __ _ ___ _ _ -| |\/| |/ _` || ' \ / _` |/ _` |/ -_)| '_| -|_| |_|\__,_||_||_|\__,_|\__, |\___||_| - |___/ -""" - base_directory = Path(__file__).parent.parent data_directory = base_directory / "data" logfile_directory = data_directory / "logs" @@ -32,7 +21,6 @@ configuration_file = data_directory / "config.yml" caddyfile = data_directory / "Caddyfile" logfile_caddy = logfile_directory / "caddy.log" logfile_app = logfile_directory / "app.log" -logfile_sessioncleanup = logfile_directory / "session-cleanup.log" class MonitoredSubprocess: @@ -91,29 +79,27 @@ def cleanup_procs(processes): def start_and_monitor(monitored_subprocesses: list): - # display banner - print(banner.format(version=os.environ["APP_VERSION"])) # start processes - for p in monitored_subprocesses: - p.try_start() - register_exithandler(cleanup_procs, monitored_subprocesses) - # monitor processes - try: - while True: - sleep(1) - for p in monitored_subprocesses: - returncode = p.s.poll() - if returncode is None: - continue - else: - print(f"{p.name} stopped with exit code {returncode}.") - if p.try_start() is False: - # stop everything if the process - # has exceeded max. tries - exit() - except KeyboardInterrupt: - print("Received KeyboardInterrupt, exiting...") - exit() + for p in monitored_subprocesses: + p.try_start() + register_exithandler(cleanup_procs, monitored_subprocesses) + # monitor processes + try: + while True: + sleep(1) + for p in monitored_subprocesses: + returncode = p.s.poll() + if returncode is None: + continue + else: + print(f"{p.name} stopped with exit code {returncode}.") + if p.try_start() is False: + # stop everything if the process + # has exceeded max. tries + exit() + except KeyboardInterrupt: + print("Received KeyboardInterrupt, exiting...") + exit() if __name__ == "__main__": @@ -131,7 +117,6 @@ if __name__ == "__main__": ["./venv/bin/python3", "./manage.py", "migrate", "--noinput"], env=os.environ).wait() # Caddy configuration via env environment_caddy = os.environ - environment_caddy["ROOTDIR"] = str(base_directory.absolute()) environment_caddy["DATADIR"] = str(data_directory.absolute()) environment_caddy["CADDY_HOSTS"] = ", ".join(config["caddy"]["hosts"]) environment_caddy["HTTP_PORT"] = str(config["caddy"]["http_port"]) @@ -185,6 +170,6 @@ if __name__ == "__main__": MonitoredSubprocess( "Session Autocleaner", ["./scripts/_session-autocleaner.py", str(config["app"]["session_clear_interval"])], - logfile_sessioncleanup) + logfile_app) ] start_and_monitor(procs) diff --git a/start.sh b/start.sh index cd50964..a7c635a 100755 --- a/start.sh +++ b/start.sh @@ -11,6 +11,6 @@ chmod -c -R g-w,o-rwx .gitignore export PYTHONPATH="$basedir" export DJANGO_SETTINGS_MODULE="project.settings" -export APP_VERSION="22" +export APP_VERSION="15" exec ./scripts/_bootstrap.py "$@" diff --git a/static/fonts/Inter-Bold.ttf b/static/fonts/Inter-Bold.ttf deleted file mode 100644 index 8e82c70..0000000 Binary files a/static/fonts/Inter-Bold.ttf and /dev/null differ diff --git a/static/fonts/Inter-Regular.ttf b/static/fonts/Inter-Regular.ttf deleted file mode 100644 index 8d4eebf..0000000 Binary files a/static/fonts/Inter-Regular.ttf and /dev/null differ diff --git a/static/js/main.js b/static/js/main.js deleted file mode 100644 index 5fab451..0000000 --- a/static/js/main.js +++ /dev/null @@ -1,21 +0,0 @@ -document.addEventListener("DOMContentLoaded", () => { - let dropdownmenuElement = document.getElementById("dropdownmenu"); - let dropdownmenuButtonElement = document.getElementById("dropdownmenu-button"); - let dropdownmenuNopeElement = document.getElementById("dropdownnope"); - function toggleDropDown() { - if (dropdownmenuElement.classList.contains("dropdownvisible")) { - dropdownmenuElement.classList.remove("dropdownvisible"); - dropdownmenuNopeElement.classList.remove("dropdownvisible"); - } else { - dropdownmenuElement.classList.add("dropdownvisible"); - dropdownmenuNopeElement.classList.add("dropdownvisible"); - } - } - if (dropdownmenuButtonElement != null) { - dropdownmenuButtonElement.addEventListener("click", toggleDropDown); - dropdownmenuNopeElement.addEventListener("click", () => { - dropdownmenuElement.classList.remove("dropdownvisible"); - dropdownmenuNopeElement.classList.remove("dropdownvisible"); - }) - } -}); diff --git a/static/material-icons/arrow-drop-down.svg b/static/material-icons/arrow-drop-down.svg deleted file mode 100644 index 051a82d..0000000 --- a/static/material-icons/arrow-drop-down.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/material-icons/menu.svg b/static/material-icons/menu.svg deleted file mode 100644 index 91b846d..0000000 --- a/static/material-icons/menu.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file