156 Commits

Author SHA1 Message Date
0d3c47d8b3 Code cleanup using panda-bears 2020-01-16 20:24:29 +01:00
95f5e0b920 small fixes for django 2.0 2019-03-14 18:41:11 +01:00
b12203911e Moved shellscript functionality to an Makefile 2019-03-14 18:38:24 +01:00
a733130b65 Changed Middleware definition to new Django style 2019-01-31 20:36:47 +01:00
483e6f215d Fixed: Middleware process_response got not executed. 2019-01-12 16:26:13 +01:00
2bdf8c9692 Workaround: get easy-thumbnails from git for django 2.1 compatibility. 2019-01-06 11:41:08 +01:00
36e24b7da8 Update dependencies to Django2
PostgreSQL psycopg2 dependecy changed to wheel distribution.
2019-01-02 12:07:52 +01:00
79a5cdb868 removed compiled translations 2019-01-02 11:55:11 +01:00
5ed8976074 fixed add_page and edit_page URL definitions to work with "/" path. 2019-01-01 18:30:52 +01:00
12930c2ec6 fixed format of <date datetime""> output. 2018-12-30 11:22:17 +01:00
e792da7459 Don't try to show the next event if we can't find one. 2018-12-30 11:20:41 +01:00
b01dc33cb4 fixed path to changed virtenv 2018-12-30 11:20:00 +01:00
1bdce47375 Default to new and empty Page Elements instead of None.
Fixes an issue when we try to acces Page.title in the template.
2018-12-29 18:11:11 +01:00
bb7d4c8a08 fix for django-extra-views 0.12.0 Updated class EventSeriesForm to use factory_kwargs. 2018-12-19 18:06:15 +01:00
a3ac722acf * Bugfix import the _() translation in mixins. 2018-06-08 09:05:45 +02:00
1313d96e8f * fixed 404 errors in the gallery. 2018-06-07 14:18:18 +02:00
f2836baae5 * removed gender, address and phone number from the forms.
* birthday, first and last name fields will be set to None if you uncheck membership.
2018-05-16 12:40:34 +02:00
d67719cc17 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	.gitignore
#	src/membership/forms.py
#	src/membership/templates/membership/membership_form.html
#	src/membership/templates/membership/register_form.html
2018-05-16 12:03:10 +02:00
12773463a6 * removed gender, address and phone number from the forms.
* birthday, first and last name fields will be set to None if you uncheck membership.
2018-05-16 11:59:08 +02:00
a8536fc776 Catch an KyuDanRanking.DoesNotExist exception if the user has never played a game of mahjongg by now. 2018-05-08 12:31:46 +02:00
ba4620705f Catch an KyuDanRanking.DoesNotExist exception if the user has never played a game of mahjongg by now. 2018-05-08 12:31:46 +02:00
4db5c09387 Merge branch 'dsgvo' 2018-05-08 12:15:12 +02:00
40d2120d7e Merge branch 'dsgvo' 2018-05-08 12:15:12 +02:00
0768641184 removed unneeded import 2018-05-08 12:12:15 +02:00
f7e9585932 removed unneeded import 2018-05-08 12:12:15 +02:00
fae5f30cfa User must confirm that he have read and understood the privacy plicity. This policity and the stautes are now linked in the help_text. 2018-05-08 12:09:56 +02:00
99ceb54db1 User must confirm that he have read and understood the privacy plicity. This policity and the stautes are now linked in the help_text. 2018-05-08 12:09:56 +02:00
e95ad8bf0a User must confirm that he have read and understood the privacy plicity. This policity and the stautes are now linked in the help_text. 2018-05-08 12:07:39 +02:00
f841c1736e User must confirm that he have read and understood the privacy plicity. This policity and the stautes are now linked in the help_text. 2018-05-08 12:07:39 +02:00
4b68f62929 removed social auth methods for login 2018-05-07 23:37:48 +02:00
436a77d036 removed social auth methods for login 2018-05-07 23:37:48 +02:00
fc8daaaf25 removed <p> Tags to guarantee vaild HTML output. 2018-05-04 12:59:38 +02:00
a10b2706c3 removed <p> Tags to guarantee vaild HTML output. 2018-05-04 12:59:38 +02:00
c3d213934e Added a themed an customizeable logged out message page. 2018-05-04 12:53:23 +02:00
afc471d27f Added a themed an customizeable logged out message page. 2018-05-04 12:53:23 +02:00
baa9660b7c fixed: load all toplevel pages from the db, but only show active pages in the top menu. 2018-05-04 11:57:19 +02:00
1d18356c8c fixed: load all toplevel pages from the db, but only show active pages in the top menu. 2018-05-04 11:57:19 +02:00
3dcd1aeffa Changed membership forms, text can now be changed via admin tool. 2018-05-04 11:34:57 +02:00
5a32dbf0af Changed membership forms, text can now be changed via admin tool. 2018-05-04 11:34:57 +02:00
5affdb0f5c Better age check 2018-05-03 16:35:40 +02:00
65b8208987 Better age check 2018-05-03 16:35:40 +02:00
9f0126be54 Dirty check club member must be at least 16 years old. 2018-05-03 16:23:39 +02:00
4f61b1e73b Dirty check club member must be at least 16 years old. 2018-05-03 16:23:39 +02:00
0f71bb5360 First draft of a disclaimer (german) 2018-05-03 10:32:27 +02:00
df4d8c83a6 First draft of a disclaimer (german) 2018-05-03 10:32:27 +02:00
3e64f4d757 You can only fill in your personal data if you apply for a full membership. 2018-05-03 10:12:23 +02:00
f2533273e9 You can only fill in your personal data if you apply for a full membership. 2018-05-03 10:12:23 +02:00
121167f1fe Added a admin action to cleanup personal data from inactive members. 2018-05-03 09:45:06 +02:00
595341a53b Added a admin action to cleanup personal data from inactive members. 2018-05-03 09:45:06 +02:00
ac9b3e06c0 Don't display full name anywhere. 2018-05-01 20:27:57 +02:00
af46768e7d Don't display full name anywhere. 2018-05-01 20:27:57 +02:00
9fdf725702 Don't display members real name anywhere! 2018-05-01 20:27:22 +02:00
10bb990539 Don't display members real name anywhere! 2018-05-01 20:27:22 +02:00
2f5f834bba Track without cookies. 2018-04-30 11:37:32 +02:00
90cefd8739 Track without cookies. 2018-04-30 11:37:32 +02:00
722c155c17 Personal data is only needed for a club membership. 2018-04-30 11:12:20 +02:00
a4c4f96c06 Personal data is only needed for a club membership. 2018-04-30 11:12:20 +02:00
3b162fc59c All personal data can only seen by yourself. 2018-04-30 10:16:04 +02:00
19bbb5a226 All personal data can only seen by yourself. 2018-04-30 10:16:04 +02:00
e9e997e38d Merge branch 'master' into css3_redesign
# Conflicts:
#	requirements/base.txt
#	src/content/locale/de/LC_MESSAGES/django.mo
#	src/content/locale/de/LC_MESSAGES/django.po
#	src/events/locale/de/LC_MESSAGES/django.mo
#	src/events/locale/de/LC_MESSAGES/django.po
#	src/kasu/locale/de/LC_MESSAGES/django.po
#	src/mahjong_ranking/locale/de/LC_MESSAGES/django.mo
#	src/mahjong_ranking/locale/de/LC_MESSAGES/django.po
#	src/maistar_ranking/locale/de/LC_MESSAGES/django.po
#	src/membership/locale/de/LC_MESSAGES/django.mo
#	src/membership/locale/de/LC_MESSAGES/django.po
#	src/utils/locale/de/LC_MESSAGES/django.po
2018-04-30 08:29:14 +02:00
3a3c8ec0c5 Merge branch 'master' into css3_redesign
# Conflicts:
#	requirements/base.txt
#	src/content/locale/de/LC_MESSAGES/django.mo
#	src/content/locale/de/LC_MESSAGES/django.po
#	src/events/locale/de/LC_MESSAGES/django.mo
#	src/events/locale/de/LC_MESSAGES/django.po
#	src/kasu/locale/de/LC_MESSAGES/django.po
#	src/mahjong_ranking/locale/de/LC_MESSAGES/django.mo
#	src/mahjong_ranking/locale/de/LC_MESSAGES/django.po
#	src/maistar_ranking/locale/de/LC_MESSAGES/django.po
#	src/membership/locale/de/LC_MESSAGES/django.mo
#	src/membership/locale/de/LC_MESSAGES/django.po
#	src/utils/locale/de/LC_MESSAGES/django.po
2018-04-30 08:29:14 +02:00
4aab611026 requirements changed to django<2.0 for compatibility reasons. 2018-04-28 11:11:12 +02:00
d5995bc612 Updated all translations 2018-04-27 10:34:37 +02:00
3ef947f128 Event gallery shows most recent albums first.
Doubled the shown events in the gallery to 24 per page.
2018-04-27 10:20:29 +02:00
28f7292c9d Fixed typo Player List (Spieler Liste) is now
Players list (Spielerliste)
2018-04-27 10:12:39 +02:00
7b631230da Added the full address to the next/upcoming event in the redbox. 2018-04-27 10:11:19 +02:00
6804319c28 adapted cleanup script to new file locations and parameters. 2018-04-27 10:10:02 +02:00
0a793b7954 Fixed: Changed the environ to ORIGINAL_RECIPIENT 2018-01-17 16:19:48 +01:00
a2df81d62e Fixed: Changed the environ to ORIGINAL_RECIPIENT 2018-01-17 16:19:48 +01:00
4b0a5c9c82 * Throw 404 instead of a 503 in Event Mixins if the related event does
not exist.
* Changes in the KyuDanRanking View to be more stable if we get bogus
kwargs.
2018-01-10 02:02:35 +01:00
afd163298c * Throw 404 instead of a 503 in Event Mixins if the related event does
not exist.
* Changes in the KyuDanRanking View to be more stable if we get bogus
kwargs.
2018-01-10 02:02:35 +01:00
6796b58d4c Merge remote-tracking branch 'origin/master' into riichi_2018
# Conflicts:
#	src/events/mixins.py
#	src/kasu/settings.py
#	src/mahjong_ranking/management/commands/export_ranking.py
#	src/mahjong_ranking/management/commands/resetdanranking.py
#	src/mahjong_ranking/management/commands/update_ranking.py
#	src/mahjong_ranking/managers.py
#	src/mahjong_ranking/models.py
#	src/mahjong_ranking/views.py
2018-01-04 08:39:49 +01:00
cce6ac6014 Merge remote-tracking branch 'origin/master' into riichi_2018
# Conflicts:
#	src/events/mixins.py
#	src/kasu/settings.py
#	src/mahjong_ranking/management/commands/export_ranking.py
#	src/mahjong_ranking/management/commands/resetdanranking.py
#	src/mahjong_ranking/management/commands/update_ranking.py
#	src/mahjong_ranking/managers.py
#	src/mahjong_ranking/models.py
#	src/mahjong_ranking/views.py
2018-01-04 08:39:49 +01:00
0b2e040fc9 * Kommentare wenn Dan/Kyu Punktabzüge verringert werden um nicht unter
0 zu fallen.
* Neue Middleware die REMOTE_IP aus dem X-Forward-For Header setzt.
  Damit funktioniert das Kommentarsystem nun auch hinter nginx.
2017-12-29 10:03:08 +01:00
192721452e * Kommentare wenn Dan/Kyu Punktabzüge verringert werden um nicht unter
0 zu fallen.
* Neue Middleware die REMOTE_IP aus dem X-Forward-For Header setzt.
  Damit funktioniert das Kommentarsystem nun auch hinter nginx.
2017-12-29 10:03:08 +01:00
fdbf819092 XLSX Export vereinheitlicht.
Spieler Hanchanlisten können nun als XLSX exportiert werden.
Anpassungen in den Einstellungen für die parametisierten Kyu/Dan Berechnung.
2017-12-26 21:45:39 +01:00
10c27784ee XLSX Export vereinheitlicht.
Spieler Hanchanlisten können nun als XLSX exportiert werden.
Anpassungen in den Einstellungen für die parametisierten Kyu/Dan Berechnung.
2017-12-26 21:45:39 +01:00
9f6fffa4f4 Noch mehr Einstellungen für Kasu Ranking, um es an das neue Dan System anpassen zu können. 2017-12-22 10:54:11 +01:00
8ab99ec039 Noch mehr Einstellungen für Kasu Ranking, um es an das neue Dan System anpassen zu können. 2017-12-22 10:54:11 +01:00
b7fab97715 Diverse Umbauarbeiten für das neue Ranking. 2017-12-22 10:51:20 +01:00
1fdf88c6d2 Diverse Umbauarbeiten für das neue Ranking. 2017-12-22 10:51:20 +01:00
c030a31e2b Pinned Django on < 2.0 for better compatibility.
Mainlined traslation code for better DRY workflow.
Fixed the EventDetail Mixin.
2017-12-07 22:54:18 +01:00
b20b988e5d Pinned Django on < 2.0 for better compatibility.
Mainlined traslation code for better DRY workflow.
Fixed the EventDetail Mixin.
2017-12-07 22:54:18 +01:00
ade2a568f7 added on_delete in models an migrations for django 2.0
compatibility.
2017-12-07 22:08:47 +01:00
cf0e5e778c added on_delete in models an migrations for django 2.0
compatibility.
2017-12-07 22:08:47 +01:00
c5781246fe Added a setting where the exported excel files should be stored.
Added a option to send the exported excel as mail attachment.
2017-12-07 09:40:35 +01:00
c7b714c41b Added a setting where the exported excel files should be stored.
Added a option to send the exported excel as mail attachment.
2017-12-07 09:40:35 +01:00
f51155cfac Fixed queryset so that EventDetailView will work too. 2017-11-24 19:48:21 +01:00
1315bc4225 Fixed Hanchan count and filter from last merge. 2017-11-24 19:47:28 +01:00
97749bfd2e Squashed commit of the following:
commit bb5081a78b
Author: Xeniac <xeniac@posteo.at>
Date:   Thu Nov 23 22:02:40 2017 +0100

    Added a setting where the exported excel files should be stored.
    Added a option to send the exported excel as mail attachment.

commit 854fd38740
Author: Xeniac <xeniac@posteo.at>
Date:   Thu Nov 23 22:01:38 2017 +0100

    Fixed: enumerate the Seasonrankings starting with 1
    Fixed: Logging error when a value changed from/to None

commit 6de1ecb102
Author: Christian Berg <xeniac@posteo.at>
Date:   Thu Nov 23 14:15:36 2017 +0100

    add a latest method to query the latest x events

commit bf12060c3b
Author: Christian Berg <xeniac@posteo.at>
Date:   Thu Nov 23 14:15:12 2017 +0100

    add a latest method to query the latest x events

commit 5ad628f33a
Author: Christian Berg <xeniac@posteo.at>
Date:   Mon Nov 20 07:47:47 2017 +0100

    Changed PlayerDanScore to only list non-legacy hanchans

commit 36272c60d6
Author: Christian Berg <xeniac@posteo.at>
Date:   Mon Nov 20 07:42:44 2017 +0100

    fixed import of MIN_HANCHANS_FOR_LADDER that moved to settings

commit c428f6ed1f
Author: Christian Berg <xeniac@posteo.at>
Date:   Mon Nov 20 07:41:04 2017 +0100

    Updated docstrings for new since and until kwargs

commit 9276e97c36
Author: Christian Berg <xeniac@posteo.at>
Date:   Mon Nov 20 07:33:54 2017 +0100

    added a since parameter to the hanchan queries to return only hanchans since the give date and time

commit fd244f10e8
Author: Christian Berg <xeniac@posteo.at>
Date:   Sun Nov 19 16:55:10 2017 +0100

    new command: resetdanranking YYYY-MM-DD, sets every dan player to 1st dan with zero dan_points at the given date.

commit 0a45cf1fd8
Author: Christian Berg <xeniac@posteo.at>
Date:   Sun Nov 19 16:14:59 2017 +0100

    added new fields to KyuDanRanking that allow to pick up the calculation from the last state of the KyuDanRanking.
    last_hanchan_date: it contains the start of the latest hanchan content for this players ranking.
    wins_in_row: to save the currents wins in a row

    Added option to calcuclate rankings until a given datetime.
2017-11-23 22:26:22 +01:00
bb5081a78b Added a setting where the exported excel files should be stored.
Added a option to send the exported excel as mail attachment.
2017-11-23 22:02:40 +01:00
84880281c6 Added a setting where the exported excel files should be stored.
Added a option to send the exported excel as mail attachment.
2017-11-23 22:02:40 +01:00
854fd38740 Fixed: enumerate the Seasonrankings starting with 1
Fixed: Logging error when a value changed from/to None
2017-11-23 22:01:38 +01:00
cffbd30d7e Fixed: enumerate the Seasonrankings starting with 1
Fixed: Logging error when a value changed from/to None
2017-11-23 22:01:38 +01:00
6de1ecb102 add a latest method to query the latest x events 2017-11-23 14:15:36 +01:00
3a611ca9da add a latest method to query the latest x events 2017-11-23 14:15:36 +01:00
bf12060c3b add a latest method to query the latest x events 2017-11-23 14:15:12 +01:00
68c484afc9 add a latest method to query the latest x events 2017-11-23 14:15:12 +01:00
5ad628f33a Changed PlayerDanScore to only list non-legacy hanchans 2017-11-20 07:47:47 +01:00
92470514c4 Changed PlayerDanScore to only list non-legacy hanchans 2017-11-20 07:47:47 +01:00
36272c60d6 fixed import of MIN_HANCHANS_FOR_LADDER that moved to settings 2017-11-20 07:42:44 +01:00
d33e5fc8c6 fixed import of MIN_HANCHANS_FOR_LADDER that moved to settings 2017-11-20 07:42:44 +01:00
c428f6ed1f Updated docstrings for new since and until kwargs 2017-11-20 07:41:04 +01:00
bb110da5a2 Updated docstrings for new since and until kwargs 2017-11-20 07:41:04 +01:00
9276e97c36 added a since parameter to the hanchan queries to return only hanchans since the give date and time 2017-11-20 07:33:54 +01:00
35a51091bf added a since parameter to the hanchan queries to return only hanchans since the give date and time 2017-11-20 07:33:54 +01:00
fd244f10e8 new command: resetdanranking YYYY-MM-DD, sets every dan player to 1st dan with zero dan_points at the given date. 2017-11-19 16:55:10 +01:00
c0c48f950a new command: resetdanranking YYYY-MM-DD, sets every dan player to 1st dan with zero dan_points at the given date. 2017-11-19 16:55:10 +01:00
0a45cf1fd8 added new fields to KyuDanRanking that allow to pick up the calculation from the last state of the KyuDanRanking.
last_hanchan_date: it contains the start of the latest hanchan content for this players ranking.
wins_in_row: to save the currents wins in a row

Added option to calcuclate rankings until a given datetime.
2017-11-19 16:14:59 +01:00
d9e0d5596c added new fields to KyuDanRanking that allow to pick up the calculation from the last state of the KyuDanRanking.
last_hanchan_date: it contains the start of the latest hanchan content for this players ranking.
wins_in_row: to save the currents wins in a row

Added option to calcuclate rankings until a given datetime.
2017-11-19 16:14:59 +01:00
a3c02ae73a FIXED: DateTimeField Hanchan.start received a naive datetime 2017-11-13 22:59:13 +01:00
e6f2528a0e Moved configuration to settings 2017-11-13 21:02:12 +01:00
638ec96c25 Code cleanup some small changes in the formatting. 2017-11-07 06:52:55 +01:00
34d327f183 Code cleanup some small changes in the formatting. 2017-11-07 06:48:14 +01:00
002eb40ea5 Fixed: SplitDateTime Widgets always complains about the date/time format. 2017-11-05 21:58:12 +01:00
d97c9db539 Fixed Typer Error "can't compare datetime.datetime to datetime.date".
Add a datetime to the hanchan queue instead of an date.
2017-11-05 10:22:39 +01:00
7ef69849a4 Sitemaps for page that should be public viewable 2017-11-03 07:16:47 +01:00
8719c2377a export_ranking now exports KyuDanRankings and SeasonRankings. 2017-11-03 07:15:28 +01:00
abeb86d48f recalculating only the new hanchans shoud do the trick now.
the racalc cronjob reports erronous partly recalculations now.
A lot of code cleanups
2017-11-01 09:53:52 +01:00
42a6ebedf9 TODO ergänzt mit meldungen von coala.io
scripte nach bin/ verschoben
2017-09-08 07:51:07 +02:00
b3ab9798b5 Another Step in the Quest to clean up the code base. 2017-09-08 07:19:50 +02:00
ce218080b2 Removed manual references on the CKEditor, they will be injected by
django-ckeditor
2017-06-26 17:12:57 +02:00
63e099b7c5 Coala .coafile hinzugefügt für einfache überwachung der Codestandards. 2017-06-09 17:19:44 +02:00
a26a91c360 Eine Menge Aufräumarbeiten.
* Eine Testsuite um Mahrjong Ranking Berechnungen zu testen
* Erste Arbeiten um die Workarounds aus dem "utils" Paket los zu werden.
* Vieles am Code umformatiert für PEP8 conformität
2017-06-07 13:25:30 +02:00
cf0bbb4c8f Abhängikeiten aktualisiert.
Social Auth Module haben den Eigentümer verändert,die Paketnamen wurden angepasst
Abhängigkeiten für Python 3 kompatible Module aktualisiert
Versionseinschränkungen für Django Update entfernt.
2017-05-10 10:21:38 +02:00
321531c160 Anpassungen des Codes an Django 1.11 mit Python 3
Grapelli wurde entfernt, das neue Django Admin ist hübsch genug.
2017-05-10 10:15:39 +02:00
5437b7b8de Neues Kommando: update-ranking
Aktualisiert alle Kyu-/Dan Rankings mit einem einfachen Befehl in der Shell.

random-ranking wurde gelöscht, es wird nicht mehr verwendet.
2017-05-10 10:13:23 +02:00
10bdaaa98c Adapted Code for Django 1.11 and Python 3,
updated the CKEditor
2017-05-10 10:10:11 +02:00
9b4ab374c6 Auswahl wird auf aktivierte Accounts mit aktiver Mitgliedschaft beschränkt. 2017-03-13 09:33:27 +01:00
ae87414584 Migration auf dne neuen Social-Auth Code. 2017-02-18 19:59:27 +01:00
120b4ea17f requirements in des Repository aufgenommen, auch diese sollter versioniert sein. 2017-02-18 19:41:27 +01:00
fcb15c7e7e * Aktelle Vorlagen in den src Ordner verschoben, damit sie in der Versionierung enthalten sind.
* INSTALLED_APPS auf PROJECT_APPS + PREREQ_APPS damit Vorlagen aus den kasu/templates Ordner bevorzugt werden. So lassen sich Vorlagen von 3rd Party Apps überschreiben, wie z.b. django-contrib-comments.

* Javascript Code für Google Maps entfernt, dies funktioniert mit Content Security Policy nicht mehr so gut und wird eh nicht gebraucht.

* Javascript für das Hanchan Formular vom header an des Ende des body gesetzt, der Code darf erst ausgeführt werden, wenn das Formular aufgebaut wurde.
2017-02-18 16:15:35 +01:00
299418ee62 Abhängikieten aktualisiert python-social-auth 0.3 und aufwärts haben fehlerhafte Abhängikeiten 2017-01-12 20:14:15 +01:00
e5f0d7f6fe renamed club.jpg to about.jpg, that's the new slug for the about page on kasu.at 2016-10-14 21:35:44 +02:00
53974dcd48 Add /rankings/ to the disallow list.
It's content that don't belong to an searchindex.
2016-10-14 21:34:39 +02:00
222dd060b1 Added Datefields for creation and last modied metadata. It's currently used for the sitemap.xml. 2016-10-14 21:27:58 +02:00
bbe16b2d13 added localized deschription fields osed for the meta description tag on the page for SEO. 2016-10-14 21:25:23 +02:00
ba44e97e9a Django Sitemap Framework integriert 2016-10-11 23:54:18 +02:00
8595959872 Added "Host: Directive" for Yandex. This line is useless, but prevents their "webmaster central" to send me unneccesary "warnings". "Host: kasu.at" literaly says that this site is available under kasu.at, not www.kasu.at 2016-09-17 10:39:01 +02:00
9b0967adc8 Some changes that the settings to reduce the logic in the local_settings.py 2016-09-16 19:37:37 +02:00
ac78dc3d75 Helptext now appears beside a Checkbox/Circle, not as a prargraph below anymore. 2016-09-16 19:36:13 +02:00
57b7d5a84b Added database migrations for the previous changes. 2016-09-16 19:34:24 +02:00
6b37aa84b0 Updated german translation 2016-09-16 19:30:09 +02:00
Christian Berg
0c7bd466c7 Kleinigkeiten und Tippfehler in den Templates für die Registrierung geändert. 2016-09-16 19:17:39 +02:00
0bdd409dc2 User Registration now uses all possibilities of the Membership Model. So you could apply for a membership with one step.
Fixed an error in the cleanup-registration command.
Also more stuff has been translated
2016-09-16 18:57:28 +02:00
Christian Berg
9c4ec20394 adding GPLv3 License to the Project 2016-09-16 18:01:58 +02:00
Christian Berg
6f1512906d Änderungen am layout wie die config Dateien abgelegt werden, um passwörter leichter aus dem öffentlichen GIT repository verschwinden zu lassen 2016-09-14 00:57:57 +02:00
Christian Berg
12c9a6e0e9 Aktuelle Fassung der Kasu.at Homepage. Kleine kosmetische Optimierungen. 2016-09-13 23:11:00 +02:00
Christian Berg
8cac20dd2b FIXED: 9th Dan is the highest possible Dan, so you can't advanced higher with 3 wins in an row. 2016-04-09 23:58:52 +02:00
Christian Berg
b1586efbab Fehler bei Vergabe von Bonuspunkte korrigiert.
Kommentare für Bonuspunkte werden jetzt als Kommentar beim Spieler hinterlassen, nicht als Kommentar in der Hanchan.
FIXED: 3_in_a_row counter wurde nicht zurückgesetzt wenn Bonuspunkte vergeben wurden.
FIXED: Durchschnittliche Platzierung während eines Events wurde nur als Ganzzahl berechnet. Wird nun als Fießkomma berechnet und gesichert.
2016-01-09 22:55:26 +01:00
Christian Berg
088efe2f39 Abhänigikeiten richtig gestellt.
Django benötigt flup. Kasu verwendet jetzt PostgreSQL, MySQL Abhängigkeit entfernt.
2016-01-09 22:48:48 +01:00
Christian Berg
b9ec418e5e 2 Fehler behoben:
1.  Internet Explorer unterstützung für <main> Tag ist fehlerhaft, wurde per CSS Definituon behoben.
2.  Templates für registrierung versehntlich gelöscht, wurden im membership Modul wieder hergestellt.
2015-08-23 21:04:20 +02:00
Christian Berg
836eee983d Milestone 08-14
* Mahjong Ranking wurde stark vereinfacht um Fehler besser vorzubeugen.
* Online WYSIWYG Editor auf CKEditor umgeändert, damit online bearbeiten für unbedarfte besser funktioniert.
* Viele kleine Optimierungen am CSS für bessere Performance.
* CSS wird jetzt aus LESS Code generiert
* Für dise Arbeit wird jetzt grunt und node package management lokal verwendet.
2015-08-23 16:37:39 +02:00
Christian Berg
79eaeb34ad Fehlerbereinigung.
Mahjong Ranking berechnet nun die richtigen Ränge zu den legendären Kyu/Dan Punkten.

Stablie Version bevor die Datenstruktur des Mahjongrankings vereinfacht wird.
2015-08-18 20:25:37 +02:00
Christian Berg
bafbf38612 Vereinfachung Membership ist nun ein custom login model, keine Abhängikeiten mehr zu auth.User.
absofort können alle Benutzer ins Ranking eingetragen werden und nicht mehr nur "zahlende Mitglieder".
2015-08-10 20:49:07 +02:00
Christian Berg
b96b485b61 Anpassungen für das Hosting bei Djangoeurope und damit verbundenen Versionen Django 1.8 und Python 2.7 2015-08-05 18:55:53 +02:00
100 changed files with 6015 additions and 2423 deletions

View File

@@ -5,4 +5,4 @@ language = python
bears = PEP8Bear,PyLintBear,PyUnusedCodeBear
use_spaces = True
pylint_cli_options = --load-plugins pylint_django
pylint_disable = E1101,R0201,R0901,R0903
#pylint_disable = E1101,R0201,R0901,R0903

2
.gitignore vendored
View File

@@ -62,6 +62,7 @@ docs/_build/
target/
#Django Development
backup/
/bower_components/
/media/
/node_modules/
@@ -70,3 +71,4 @@ target/
.[a-zA-Z]*
local_settings.py
sample.xlsx
venv/

View File

@@ -8,7 +8,7 @@ module.exports = function(grunt) {
options: {
paths: ['src/kasu/static/less'],
compress: false,
optimization:9,
optimization: 9,
ieCompat: false,
},
kasu: {
@@ -21,10 +21,11 @@ module.exports = function(grunt) {
report: 'min'
},
kasu: {
src: 'static/css/kasu.css',
dest: 'static/css/kasu.css'
}
},
files: {
'src/kasu/static/css/kasu.min.css': ['src/kasu/static/css/kasu.css'],
},
},
},
watch: {
styles: {
files: ['src/kasu/static/less/*.less'], // which files to watch

80
Makefile Normal file
View File

@@ -0,0 +1,80 @@
ASSESTS=requirements static
DEV_REQUIREMENTS=requirements/development.txt
DJANGO_SETTINGS_MODULE="kasu.settings"
EXCLUDE_FILES=*.pyc
PROJECT_PATH=$(CURDIR)
PYTHON=${VENV_PATH}/bin/python3
REQUIREMENTS=requirements/base.txt
SRC_PATH=${PROJECT_PATH}/src
MANAGE_PY=${PYTHON} -Wa ${SRC_PATH}/manage.py
SSH_LOGIN=kasu@kasu.at
VENV_PATH=$(PROJECT_PATH)/venv
.PHONY: clean venv
all: cleanup migrate testserver
venv: $(VENV_PATH)/bin/activate
$(VENV_PATH)/bin/activate:
@test -d $(VENV_PATH) || python3 -m venv --clear $(VENV_PATH)
dev-requirements: venv ${DEV_REQUIREMENTS}
${PYTHON} -m pip install -qU pip
${PYTHON} -m pip install -qUr ${DEV_REQUIREMENTS}
requirements: venv ${REQUIREMENTS}
${PYTHON} -m pip install -qU pip
${PYTHON} -m pip install -qUr ${REQUIREMENTS}
grunt:
-grunt
requirements_remote:
@echo "Installing requirements"
ssh ${SSH_LOGIN} "virtualenv/bin/pip install -qUr ${REQUIREMENTS}"
sync_assets:
@echo "Syncing project assets ..."
rsync -qr --copy-links --delete ${ASSESTS} ${SSH_LOGIN}:~/
sync_src:
@echo "Syncing Sourcecode ..."
find . -name EXCLUDE_FILES -exec rm -rf {} \;
rsync -qr --copy-links --delete ${SRC_PATH} ${SSH_LOGIN}:~/ --exclude 'src/kasu/local_settings.py'
restart_remote:
@echo "Rebuild and reload django..."
ssh ${SSH_LOGIN} "~/virtualenv/bin/python ~/src/manage.py collectstatic -l --noinput -v0"
ssh ${SSH_LOGIN} "~/init/kasu restart"
sync: sync_assets requirements_remote sync_src restart_remote
testserver: venv
${MANAGE_PY} runserver 127.0.0.1:8000
messages: venv
@echo "aktualisiere Übersetzungen..."
@(for d in ${SRC_PATH}/*; do \
test -d $$d/locale && cd $$d && ${VENV_PATH}/bin/django-admin.py makemessages -l de;\
done)
${MANAGE_PY} compilemessages -v0
cleanup-pyc:
@echo "lösche den Python Compiler Cache..."
find ${SRC_PATH} -name ${EXCLUDE_FILES} -exec rm -rf {} \;
collectstatic: venv
@echo "aktualisiere Statics"
${MANAGE_PY} collectstatic --noinput -c -v0
thumbnails: venv
@echo "Erstelle Vorschaubilder"
${MANAGE_PY} thumbnail_cleanup
cleanup: requirements messages cleanup-pyc collectstatic thumbnails
migrate: venv
${MANAGE_PY} migrate
migrations: venv
${MANAGE_PY} makemigrations

1077
TODO

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +0,0 @@
#!/bin/bash
source .virtualenv/bin/activate
echo "aktualisiere Übersetzungen..."
cd src
unset DJANGO_SETTINGS_MODULE
for dir in *
do
if [ -d ${dir}/locale ]
then
echo -n "$dir: "
cd ${dir}
django-admin.py makemessages -l de
cd ..
fi
done
sleep 5s
export DJANGO_SETTINGS_MODULE="kasu.settings"
./manage.py compilemessages
echo "lösche den Python Compiler Cache..."
find . -name "*.pyc" -exec rm -rf {} \;
echo "Aktualisiere Statics"
./manage.py collectstatic --noinput -c
echo "Erstelle Vorschaubilder"
./manage.py thumbnail_cleanup
touch kasu/wsgi.py

View File

@@ -1,23 +0,0 @@
#!/bin/bash
SSH_LOGIN="kasu@s21.wservices.ch"
SYNC_ASSESTS="requirements"
SYNC_SOURCECODE="src"
EXCLUDE_FILES="*.pyc"
grunt
echo "Syncing project assets ..."
rsync -r --copy-links --delete ${SYNC_ASSESTS} ${SSH_LOGIN}:~/
echo "Installing dependecies"
ssh ${SSH_LOGIN} "virtualenv/bin/pip install --upgrade -r requirements/base.txt"
echo "Syncing Sourcecode ..."
find . -name "*.pyc" -exec rm -rf {} \;
rsync -r --copy-links --delete ${SYNC_SOURCECODE} ${SSH_LOGIN}:~/ --exclude 'src/kasu/local_settings.py'
echo "Rebuild and reload django..."
ssh ${SSH_LOGIN} "rm src/kasu/settings/development.*"
ssh ${SSH_LOGIN} "virtualenv/bin/python ~/src/manage.py collectstatic -l --noinput -v1"
ssh ${SSH_LOGIN} "~/init/kasu restart"

View File

@@ -1,6 +0,0 @@
#!/bin/sh
export DJANGO_SETTINGS_MODULE=kasu.settings
source .virtualenv/bin/activate
./manage.py runserver 0.0.0.0:8000

View File

@@ -8,7 +8,7 @@
"grunt": ">=0.4.5",
"grunt-contrib-less": ">=1.0.1",
"grunt-contrib-watch": ">=0.6.1",
"grunt-more-css": ">=0.1.0"
"grunt-more-css": "^0.1.1"
},
"dependencies": {
"ckeditor-dev": "git://github.com/ckeditor/ckeditor-dev.git"

View File

@@ -1,5 +1,5 @@
beautifulsoup4
django<2.0
django < 3.0
django-appconf
django-ckeditor
django-contrib-comments
@@ -8,14 +8,13 @@ django-compressor
django-extra-views
django-markdown
django-recaptcha
easy-thumbnails
git+https://github.com/SmileyChris/easy-thumbnails.git
icalendar
openpyxl
markdown
pillow
psycopg2
psycopg2-binary
PyJWT
pytz
requests
requests-oauthlib
social-auth-app-django
social-auth-core

View File

@@ -4,5 +4,5 @@ django-rosetta
sqlparse
# Code Linting
coala-bears
pylint>=2.0
pylint-django

View File

@@ -3,6 +3,7 @@
from django.core.cache import cache
from . import models
from utils import STATUS_PUBLISHED
def content_menus(request):
@@ -18,8 +19,8 @@ def content_menus(request):
:param request: a Django request object
:return: a dict with the template variables mentioned above
"""
current_page = None
current_top_page = None
current_page = models.Page()
current_top_page = models.Page()
current_path = request.path_info[1:request.path_info.rfind('.')]
# erzeuge das Top-Level Menü
@@ -50,7 +51,7 @@ def content_menus(request):
break
current_path = current_path[0:current_path.rfind('.')]
return {'top_menu_items': top_level_pages,
return {'top_menu_items': top_level_pages.filter(status=STATUS_PUBLISHED),
'current_top_page': current_top_page,
'current_path': current_path,
'current_page': current_page}

View File

@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: kasu.content\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-04-27 09:49+0200\n"
"PO-Revision-Date: 2018-04-27 10:05+0105\n"
"POT-Creation-Date: 2019-12-13 23:38+0100\n"
"PO-Revision-Date: 2018-01-12 15:25+0105\n"
"Last-Translator: b'Christian Berg <kasu@xendynastie.at>'\n"
"Language-Team: Deutsch <>\n"
"Language: de\n"
@@ -32,130 +32,125 @@ msgstr "Neueste Kommentare auf Kasu.at "
msgid "Kasu - latest comments"
msgstr "Kasu - neue Kommentare"
#: forms.py:57 models.py:315
#: forms.py:57 models.py:318
msgid "Please upload a PDF-File to this PDF-Page."
msgstr "Bitte eine PDF Datei für diese PDF Seite hochladen."
#: models.py:68
#: models.py:76
msgid "Headline"
msgstr "Schlagzeile"
#: models.py:70
#: models.py:78
msgid "Content"
msgstr "Inhalt"
#: models.py:72 models.py:143 templates/content/article_detail.html:25
#: models.py:82 models.py:150 templates/content/article_detail.html:28
msgid "Category"
msgstr "Kategorie"
#: models.py:73 models.py:136
#: models.py:83 models.py:143
msgid "Image"
msgstr "Bild"
#: models.py:75 models.py:138
#: models.py:85 models.py:145
msgid "Slug"
msgstr "Slug"
#: models.py:77 templates/content/article_detail.html:23
#: models.py:88 templates/content/article_detail.html:21
msgid "Author"
msgstr "Autor"
#: models.py:78
#: models.py:89
msgid "Status"
msgstr "Status"
#: models.py:80
#: models.py:91
msgid "Created"
msgstr "Erstellt"
#: models.py:81
#: models.py:92
msgid "Modified"
msgstr "Bearbeitet"
#: models.py:86
#: models.py:97
msgid "Article"
msgstr "Artikel"
#: models.py:87
#: models.py:98
msgid "Articles"
msgstr "Artikel"
#: models.py:132 models.py:133
#: models.py:139 models.py:140
msgid "Name"
msgstr "Name"
#: models.py:134 models.py:135
#: models.py:141 models.py:142
msgid "Description"
msgstr "Beschreibung"
#: models.py:144
#: models.py:151
msgid "Categories"
msgstr "Kategorien"
#: models.py:176 models.py:182
#: models.py:182 models.py:188
msgid "The short name for the menu-entry of this page"
msgstr "Ein kurzer Name für den Menüeintrag"
#: models.py:187 models.py:192
#: models.py:193 models.py:198
msgid "The page title as you'd like it to be seen by the public"
msgstr "Der Seitentitel der öffentlich gemacht wird."
msgstr "Der Seitentitel der öffentlich angezeigt werden soll"
#: models.py:194
#: models.py:200
msgid "slug"
msgstr "Slug"
#: models.py:197
#: models.py:203
msgid ""
"The name of the page as it will appear in URLs e.g "
"http://domain.com/blog/[my-slug]/"
msgstr ""
"Der Seitenname wie er in der URL erscheint. z.B: "
"http://domain.com/blog/[slug]/"
"The name of the page as it will appear in URLs e.g http://domain.com/blog/"
"[my-slug]/"
msgstr "Wie die Seite in der URL aufscheint also http://domain.com/blog/[slug]"
#: models.py:206
#: models.py:212
msgid "Path"
msgstr "Pfad"
#: models.py:218
#: models.py:224
msgid "Position"
msgstr "Position"
#: models.py:223
#: models.py:229
msgid "status"
msgstr "Status"
#: models.py:226 models.py:228
#| msgid "Description"
#: models.py:232 models.py:234
msgid "search description"
msgstr "Suchbeschreibung"
msgstr "Beschreibung für Suchfunktion"
#: models.py:231
#| msgid "Content"
#: models.py:237
msgid "content type"
msgstr "Inhaltstyp"
#: models.py:236
#: models.py:242
msgid "enable comments"
msgstr "Kommentare möglich"
#: models.py:241
#: models.py:247
msgid "Template"
msgstr "Vorlage"
#: models.py:249
#| msgid "created on"
#: models.py:255
msgid "first created at"
msgstr "erstellt am"
#: models.py:254
#: models.py:260
msgid "latest updated at"
msgstr "letzte Änderung"
msgstr "letzte Aktualisierung am"
#: models.py:328
#: models.py:331
msgid "Page"
msgstr "Seite"
#: models.py:329
#: models.py:332
msgid "Pages"
msgstr "Seiten"
@@ -203,15 +198,15 @@ msgstr "neuer Artikel "
msgid "back"
msgstr "Zurück"
#: templates/content/article_detail.html:24
#: templates/content/article_detail.html:25
msgid "Created on"
msgstr "Erstellt am"
#: templates/content/article_detail.html:36
#: templates/content/article_detail.html:39
msgid "share on"
msgstr "Teile auf"
#: templates/content/article_detail.html:51 views.py:156
#: templates/content/article_detail.html:48 views.py:156
msgid "Edit Article"
msgstr "Artikel bearbeiten"
@@ -258,12 +253,12 @@ msgstr "Diese Kategorie existiert nicht."
msgid "Create Article"
msgstr "Artikel erstellen"
#: views.py:233
#: views.py:237
#, python-format
msgid "No Page found matching the Path %s"
msgstr "Keine Seite unter dem Pfad %s gefunden"
#: views.py:262
#: views.py:266
#, python-format
msgid "No PDF Document found matching the Path %s"
msgstr "Kein PDF Dokument unter dem Pfad %s gefunden."

View File

@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.db.models.deletion
from django.conf import settings
from django.db import models, migrations
class Migration(migrations.Migration):
@@ -44,7 +44,8 @@ class Migration(migrations.Migration):
('date_modified', models.DateTimeField(
auto_now=True, verbose_name='Bearbeitet')),
('author', models.ForeignKey(
verbose_name='Autor', to=settings.AUTH_USER_MODEL)),
verbose_name='Autor', to=settings.AUTH_USER_MODEL,
on_delete=models.CASCADE))
],
options={
'ordering': ('-date_created',),
@@ -144,7 +145,8 @@ class Migration(migrations.Migration):
model_name='article',
name='category',
field=models.ForeignKey(
verbose_name='Kategorie', to='content.Category'),
verbose_name='Kategorie', to='content.Category',
on_delete=models.CASCADE),
),
migrations.AlterUniqueTogether(
name='page',

View File

@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.8 on 2017-12-14 11:15
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('content', '0006_auto_20171115_0653'),
]
operations = [
migrations.AlterField(
model_name='article',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='Autor'),
),
migrations.AlterField(
model_name='article',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='content.Category', verbose_name='Kategorie'),
),
]

View File

@@ -0,0 +1,58 @@
# Generated by Django 2.1.5 on 2019-01-06 18:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('content', '0007_auto_20171214_1215'),
]
operations = [
migrations.AlterField(
model_name='article',
name='status',
field=models.SmallIntegerField(choices=[(-1, 'Rejected'), (0, 'Waiting...'), (1, 'Published')], default=1, verbose_name='Status'),
),
migrations.AlterField(
model_name='page',
name='date_created',
field=models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='erstellt am'),
),
migrations.AlterField(
model_name='page',
name='date_modified',
field=models.DateTimeField(auto_now=True, verbose_name='letzte Aktualisierung am'),
),
migrations.AlterField(
model_name='page',
name='description_de',
field=models.TextField(blank=True, verbose_name='Beschreibung für Suchfunktion'),
),
migrations.AlterField(
model_name='page',
name='description_en',
field=models.TextField(blank=True, verbose_name='Beschreibung für Suchfunktion'),
),
migrations.AlterField(
model_name='page',
name='slug',
field=models.SlugField(help_text='Wie die Seite in der URL aufscheint also http://domain.com/blog/[slug]', max_length=100, verbose_name='Slug'),
),
migrations.AlterField(
model_name='page',
name='status',
field=models.SmallIntegerField(choices=[(-1, 'Rejected'), (0, 'Waiting...'), (1, 'Published')], default=0, verbose_name='Status'),
),
migrations.AlterField(
model_name='page',
name='title_de',
field=models.CharField(help_text='Der Seitentitel der öffentlich angezeigt werden soll', max_length=255, verbose_name='Titel'),
),
migrations.AlterField(
model_name='page',
name='title_en',
field=models.CharField(blank=True, help_text='Der Seitentitel der öffentlich angezeigt werden soll', max_length=255, verbose_name='Title'),
),
]

View File

@@ -3,9 +3,9 @@ from ckeditor_uploader.fields import RichTextUploadingField
from django.conf import settings
from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.db import models
from django.template.defaultfilters import slugify
from django.urls import reverse
from django.utils import timezone
from django.utils.safestring import mark_safe
from django.utils.translation import get_language, ugettext as _
@@ -41,6 +41,14 @@ def get_upload_path(instance, filename):
return "categories/%s.%s" % (instance.slug, extension)
def get_localized(obj, attr):
""" Return the localilzed field, or the fallback if the localized is empty.
"""
fallback = attr + '_de'
localized = attr + '_' + get_language()[:2]
return getattr(obj, localized) or getattr(obj, fallback)
class ArticleManager(models.Manager):
"""Adds some predifined querys and joins some tables for faster querys."""
@@ -69,11 +77,14 @@ class Article(models.Model):
headline_en = models.CharField('Headline', max_length=255, blank=True)
content_de = RichTextUploadingField(_('Content'))
content_en = RichTextUploadingField('Content', blank=True)
category = models.ForeignKey('Category', verbose_name=_('Category'))
category = models.ForeignKey('Category',
on_delete=models.PROTECT,
verbose_name=_('Category'))
image = models.ImageField(_('Image'), upload_to='news/',
blank=True, null=True)
slug = models.SlugField(_('Slug'), unique_for_month='date_created')
author = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
verbose_name=_('Author'))
status = models.SmallIntegerField(_('Status'), choices=STATUS_CHOICES,
default=STATUS_PUBLISHED)
@@ -115,16 +126,12 @@ class Article(models.Model):
@property
def headline(self):
"""Return the localized headline, fallback to german if necessary."""
return mark_safe(
getattr(self, "headline_%s" % get_language(), self.headline_de)
)
return mark_safe(get_localized(self, 'headline'))
@property
def content(self):
"""Return the localized content, fallback to german if necessary."""
return mark_safe(
getattr(self, "content_%s" % get_language(), self.content_de)
)
return mark_safe(get_localized(self, 'content'))
class Category(models.Model):
@@ -146,13 +153,12 @@ class Category(models.Model):
@property
def name(self):
"""Return the localized name, fallback to german if necessary."""
return getattr(self, "name_%s" % get_language(), self.name_de)
return get_localized(self, 'name')
@property
def description(self):
"""Return the localized description, fallback to german if necessary."""
return getattr(self, "description_%s" % get_language(),
self.description_de)
return get_localized(self, 'description')
def get_absolute_url(self):
"""Return the URL of the article archive, filtered on this category."""
@@ -261,9 +267,7 @@ class Page(models.Model):
@property
def content(self):
"""Return the localized content, fallback to german if necessary."""
return mark_safe(
getattr(self, "content_%s" % get_language(), self.content_de)
)
return mark_safe(get_localized(self, 'content'))
@property
def css_class(self):
@@ -275,23 +279,22 @@ class Page(models.Model):
@property
def description(self):
"""Return the localized description, fallback to german if necessary."""
return getattr(self, "description_%s" % get_language(),
self.description_de)
return get_localized(self, 'description')
@property
def menu_name(self):
"""Return the localized menu name, fallback to german if necessary."""
return getattr(self, "menu_name_%s" % get_language(), self.menu_name_de)
return get_localized(self, 'menu_name')
@property
def pdf_file(self):
"""Return the localized PDF file, fallback to german if necessary."""
return getattr(self, "pdf_%s" % get_language(), self.pdf_de)
return get_localized(self, 'pdf_file')
@property
def title(self):
"""Return the localized title, fallback to german if necessary."""
return getattr(self, "title_%s" % get_language(), self.title_de)
return get_localized(self, 'title')
def clean(self):
"""set the URL path, the right content type, and scrub the HTML code."""

View File

@@ -69,7 +69,7 @@
<h3><a href="{{article.get_absolute_url}}">{{article.headline}}</a></h3>
<ul class="info">
<li><span class="fa fa-calendar-o" title="{% trans 'created on' %}"></span> <time
datetime="{{article.date_created|date:'c'}}">{{ article.date_created|date }}</time></li>
datetime="{{article.date_created|date:'Y-m-d\TH:i:sO'}}">{{ article.date_created|date }}</time></li>
<li><span class="fa fa-user" title="{% trans 'by' %}"></span> {{ article.author }}</li>
<li><span class="fa fa-comments" title="{% trans 'comments' %}"></span> <a
href="{{article.get_absolute_url}}#comments" >{{comment_count}} {% trans "comments" %}</a></li>

View File

@@ -1,53 +1,48 @@
{% extends "base.html" %}
{% load i18n comments thumbnail %}
{% extends "base.html" %}{% load i18n comments thumbnail %}
{% block title %}{{ article.headline }}{% endblock %}
{% block description %}{{article.content|striptags|truncatewords:16}}{% endblock %}
{% block opengraph %}
<meta property="og:type" content="article" />
<meta property="og:title" content="{{ article.headline|force_escape }}" />
<meta property="og:url" content="http://www.kasu.at{{ article.get_absolute_url }}" />
<meta property="og:image" content="http://www.kasu.at{{article.get_image|thumbnail_url:'article'}}" />
<meta property="og:description" content="{{article.content|striptags|truncatewords:25|force_escape}}" />
<link rel="image_src" type="image/jpeg" href="{{article.get_image|thumbnail_url:'article'}}" />
{% endblock %}
<meta property="og:type" content="article"/>
<meta property="og:title" content="{{ article.headline|force_escape }}"/>
<meta property="og:url" content="http://www.kasu.at{{ article.get_absolute_url }}"/>
<meta property="og:image" content="http://www.kasu.at{{article.get_image|thumbnail_url:'article'}}"/>
<meta property="og:description" content="{{article.content|striptags|truncatewords:25|force_escape}}"/>
<link rel="image_src" type="image/jpeg" href="{{article.get_image|thumbnail_url:'article'}}"/>{% endblock %}
{% block itemscope %}itemscope itemtype="http://schema.org/Article"{% endblock %}
{% block teaser %}
<h1 itemprop="name">{{article.headline}}</h1>
<div id="teaser_text">
<ul class="info">
<li><span class="fa fa-user"></span> <strong>{% trans 'Author' %}:</strong> <a href="{% url 'membership-details' article.author %}" itemprop="author">{{article.author}}</a></li>
<li><span class="fa fa-calendar-o"></span> <strong>{% trans 'Created on' %}: </strong><time datetime="{{article.date_created|date:'Y-m-d H:i'}}">{{ article.date_created|date:'DATE_FORMAT' }}</time></li>
<li><span class="fa fa-tag"></span> <strong>{% trans "Category"%}: </strong><a href="{{ article.category.get_absolute_url }}" itemprop="articleSection">{{article.category.name}}</a></li>
</ul>
</div>
{% endblock %}
<ul class="info">
<li><span class="fa fa-user"></span> <strong>{% trans 'Author' %}:</strong>
<a href="{% url 'membership-details' article.author %}" itemprop="author">{{article.author}}</a>
</li>
<li><span class="fa fa-calendar-o"></span>
<strong>{% trans 'Created on' %}:</strong>
<time datetime="{{article.date_created|date:'Y-m-d\TH:i:sO'}}">{{ article.date_created|date:'DATE_FORMAT' }}</time>
</li>
<li><span class="fa fa-tag"></span> <strong>{% trans "Category"%}:</strong>
<a href="{{ article.category.get_absolute_url }}" itemprop="articleSection">{{article.category.name}}</a>
</li>
</ul>
</div>{% endblock %}
{% block maincontent %}
<div itemprop="articleBody" class="grid_12">
<img alt="{{article.category.name}}" src="{{article.get_image|thumbnail_url:'article'}}" class="posting_image" itemprop="image"/>
{{ article.content }}
<img alt="{{article.category.name}}" src="{{article.get_image|thumbnail_url:'article'}}" class="posting_image" itemprop="image"/> {{ article.content }}
</div>
<p class="right">
<strong>{% trans 'share on' %}:</strong>
<a class="button" href="https://plus.google.com/share?url=http%3A%2F%2Fwww.kasu.at{{article.get_absolute_url|urlencode}}"
onclick="javascript:window.open(this.href,'', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=600,width=600');return false;"><span class="fa fa-google-plus"></span> Google+</a>
<a class="button" href="https://twitter.com/share?url=http%3A%2F%2Fwww.kasu.at{{article.get_absolute_url|urlencode}}" target='_blank'><span class="fa fa-twitter"></span> Twitter</a>
<a class="button" href="http://facebook.com/sharer.php?u=http%3A%2F%2Fwww.kasu.at{{article.get_absolute_url|urlencode}}?t={{article.headline|urlencode}}" target="_blank"><span class="fa fa-facebook"></span> Facebook</a>
</p>
{% endblock %}
<p class="right">
<strong>{% trans 'share on' %}:</strong>
<a class="button" href="https://plus.google.com/share?url=http%3A%2F%2Fwww.kasu.at{{article.get_absolute_url|urlencode}}" onclick="javascript:window.open(this.href,'', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=600,width=600');return false;"><span class="fa fa-google-plus"></span> Google+</a>
<a class="button" href="https://twitter.com/share?url=http%3A%2F%2Fwww.kasu.at{{article.get_absolute_url|urlencode}}" target='_blank'><span class="fa fa-twitter"></span> Twitter</a>
<a class="button" href="http://facebook.com/sharer.php?u=http%3A%2F%2Fwww.kasu.at{{article.get_absolute_url|urlencode}}?t={{article.headline|urlencode}}" target="_blank"><span class="fa fa-facebook"></span> Facebook</a>
</p>{% endblock %}
{% block comments%}
{% render_comment_list for article %}
{% render_comment_form for article %}
{% endblock %}
{% block comments%}{% render_comment_list for article %}{% render_comment_form for article %}{% endblock %}
{% block buttonbar %}
{% if perms.content.change_article %}
<a href="{% url 'edit-article' article.id %}" class="button"><span class="fa fa-pencil"></span> {% trans "Edit Article" %}</a>
{% endif %}
{% endblock %}
{% block buttonbar %}{% if perms.content.change_article %}
<a href="{% url 'edit-article' article.id %}" class="button"><span class="fa fa-pencil"></span> {% trans "Edit Article" %}</a>{% endif %}{% endblock %}

View File

@@ -14,7 +14,7 @@ from . import models, forms
class WYSIWYGEditorMixin(PermissionRequiredMixin):
"""
A view to update the Content-Security-Policy for the WYSIWYG editor.
Since it is only used in edit forms, it extends the PermissionRequiredMixin.
Since it is only used in edit forms, it extends the PermissionRequiredMixin.
"""
@csp_update(SCRIPT_SRC="'unsafe-eval'")
@@ -160,8 +160,8 @@ class ArticleForm(WYSIWYGEditorMixin, generic.UpdateView):
def get_object(self, queryset=None):
"""Get the Article or create a new one if no id has been provided.
:param queryset: Get the single item from this filtered queryset.
:return:
:param queryset: Get the single item from this filtered queryset.
:return:
"""
queryset = queryset or self.get_queryset()
if self.kwargs.get('pk'):
@@ -170,7 +170,7 @@ class ArticleForm(WYSIWYGEditorMixin, generic.UpdateView):
class PageAddForm(WYSIWYGEditorMixin, generic.CreateView):
""" Renders an Form to create a new page for users with conforming
""" Renders an Form to create a new page for users with conforming
permissions."""
form_class = forms.PageForm
@@ -179,13 +179,16 @@ class PageAddForm(WYSIWYGEditorMixin, generic.CreateView):
def get_initial(self):
"""Try to get the path of the parent page as initial data."""
path = os.path.splitext(self.kwargs['path'])[0]
if path.startswith('/'):
path = path[1:]
if path.endswith('/'):
path = path[:-1]
parent = models.Page.objects.get(path=path)
return {'parent': parent}
if self.kwargs['path']:
path = os.path.splitext(self.kwargs['path'])[0]
if path.startswith('/'):
path = path[1:]
if path.endswith('/'):
path = path[:-1]
parent = models.Page.objects.get(path=path)
return {'parent': parent}
else:
return None
class PageEditForm(WYSIWYGEditorMixin, generic.UpdateView):
@@ -197,14 +200,15 @@ class PageEditForm(WYSIWYGEditorMixin, generic.UpdateView):
def get_object(self, queryset=None):
""" Get the path from the URL and fetch the corresponding page.
First get the path wihout fileextentsion leading or trailing slashes,
then search in the database if such a page exists.
:param queryset: Get the single item from this filtered queryset.
:return:
:return:
"""
path = os.path.splitext(self.kwargs['path'])[0]
path = self.kwargs['path'] or 'index.html'
path = os.path.splitext(path)[0]
queryset = queryset or self.get_queryset()
if path.startswith('/'):
path = path[1:]
@@ -222,7 +226,7 @@ class PageHtml(generic.DetailView):
"""Get the page content from the db that equals the given URL.
:param queryset: Get the single item from this filtered queryset.
:return:
:return:
"""
queryset = queryset or self.get_queryset()
try:
@@ -248,10 +252,10 @@ class PagePdf(generic.DeleteView):
model = models.Page
def get_object(self, queryset=None):
"""Get the PDF page from the db that equals the given URL.
"""Get the PDF page from the db that equals the given URL.
:param queryset: Get the single item from this filtered queryset.
:return: models.Page object or raise a 404 if not found.
:return: models.Page object or raise a 404 if not found.
"""
queryset = queryset or self.get_queryset()
try:
@@ -288,8 +292,8 @@ class StartPage(generic.TemplateView):
def get_context_data(self):
""" Adds recent ariticles and recent comments to the context.
:return: array() with the context data
:return: array() with the context data
"""
page = models.Page.objects.get(slug='index')
recent_comment_list = comments.get_model().objects.filter(

View File

@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: kasu.events\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-04-27 09:49+0200\n"
"PO-Revision-Date: 2018-04-27 10:29+0105\n"
"POT-Creation-Date: 2019-12-13 23:38+0100\n"
"PO-Revision-Date: 2018-01-12 15:25+0105\n"
"Last-Translator: b'Christian Berg <kasu@xendynastie.at>'\n"
"Language-Team: Kasu <verein@kasu.at>\n"
"Language: de\n"
@@ -35,7 +35,7 @@ msgstr "Beginn"
msgid "end"
msgstr "Ende"
#: mixins.py:57 views.py:149
#: mixins.py:76
msgid "Event does not exist"
msgstr "Veranstaltung gibt es nicht"
@@ -92,13 +92,13 @@ msgstr ""
"Wenn dieser Termin zu einer Veranstaltungsreihe gehört werden Ort, "
"Beschreibung, Bild und Homepage von dem hier angegebenen Event übernommen."
#: models.py:92 models.py:195 models.py:247
#: models.py:92 models.py:195 models.py:248
msgid "first created at"
msgstr "Erstellt am"
msgstr "erstellt am"
#: models.py:97 models.py:200 models.py:252
#: models.py:97 models.py:200 models.py:253
msgid "latest updated at"
msgstr "Geändert am"
msgstr "letzte Aktualisierung am"
#: models.py:103
msgid "Event"
@@ -136,28 +136,28 @@ msgstr "Veranstaltungsort"
msgid "Venues"
msgstr "Veranstaltungsorte"
#: models.py:231
#: models.py:232
msgid "Startpage"
msgstr "Startseite"
#: models.py:234
#: models.py:235
msgid "Display this Photo on the Startpage Teaser"
msgstr "Foto als Teaser auf der Startseite verwenden."
#: models.py:236
#: models.py:237
msgid "Published on"
msgstr "Veröffentlicht am"
#: models.py:238
#: models.py:239
msgid "Number of views"
msgstr "Wie oft gesehen"
#: models.py:262 templates/events/event_archive.html:38
#: models.py:263 templates/events/event_archive.html:38
#: templates/events/event_list.html:18
msgid "Event Image"
msgstr "Veranstaltungsbild"
#: models.py:263
#: models.py:264
msgid "Event Images"
msgstr "Veranstaltungsbilder"
@@ -165,9 +165,8 @@ msgstr "Veranstaltungsbilder"
msgid "Event Archive"
msgstr "Veranstaltungsarchiv"
#: templates/events/event_archive.html:42
#: templates/events/event_detail.html:85 templates/events/event_list.html:22
#: templates/events/photo_detail.html:53
#: templates/events/event_archive.html:42 templates/events/event_detail.html:85
#: templates/events/event_list.html:22 templates/events/photo_detail.html:53
msgid "Date"
msgstr "Datum"
@@ -175,19 +174,17 @@ msgstr "Datum"
msgid "Time"
msgstr "Zeit"
#: templates/events/event_archive.html:49
#: templates/events/photo_upload.html:16
#: templates/events/event_archive.html:49 templates/events/photo_upload.html:16
msgid "from"
msgstr "von"
#: templates/events/event_archive.html:49
#: templates/events/photo_upload.html:16
#: templates/events/event_archive.html:49 templates/events/photo_upload.html:16
msgid "to"
msgstr "bis"
#: templates/events/event_archive.html:57
#: templates/events/event_detail.html:31 templates/events/event_detail.html:72
#: templates/events/event_list.html:32 templates/events/photo_upload.html:23
#: templates/events/event_archive.html:57 templates/events/event_detail.html:31
#: templates/events/event_detail.html:72 templates/events/event_list.html:32
#: templates/events/photo_upload.html:23
msgid "Location"
msgstr "Ort"
@@ -196,16 +193,15 @@ msgstr "Ort"
msgid "Comments"
msgstr "Kommentare"
#: templates/events/event_archive.html:59
#: templates/events/event_detail.html:36 templates/events/event_detail.html:48
#: templates/events/photo_list.html:4 templates/events/photo_upload.html:28
#: templates/events/photo_upload.html:29
#: templates/events/event_archive.html:59 templates/events/event_detail.html:36
#: templates/events/event_detail.html:48 templates/events/photo_list.html:4
#: templates/events/photo_upload.html:28 templates/events/photo_upload.html:29
msgid "Photos"
msgstr "Fotos"
#: templates/events/event_archive.html:60
#: templates/events/event_archive.html:61
#: templates/events/event_detail.html:35 templates/events/event_detail.html:51
#: templates/events/event_archive.html:61 templates/events/event_detail.html:35
#: templates/events/event_detail.html:51
msgid "Hanchans"
msgstr "Hanchans"

View File

@@ -1,14 +1,14 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import events.models
import django.db.models.deletion
from django.db import models, migrations
import events.models
import utils
class Migration(migrations.Migration):
dependencies = [
]
@@ -17,7 +17,8 @@ class Migration(migrations.Migration):
name='Event',
fields=[
('id', models.AutoField(verbose_name='ID',
serialize=False, auto_created=True, primary_key=True)),
serialize=False, auto_created=True,
primary_key=True)),
('name', models.CharField(max_length=255, verbose_name='Name')),
('description', models.TextField(
verbose_name='Beschreibung', blank=True)),
@@ -26,13 +27,20 @@ class Migration(migrations.Migration):
null=True, verbose_name='Ende', blank=True)),
('url', models.URLField(verbose_name='Homepage', blank=True)),
('image', models.ImageField(storage=utils.OverwriteStorage(
), upload_to=events.models.get_upload_path, null=True, verbose_name='Bild', blank=True)),
), upload_to=events.models.get_upload_path, null=True,
verbose_name='Bild', blank=True)),
('is_tournament', models.BooleanField(default=False,
help_text='Diese Veranstaltung ist ein Turnier, es gelten andere Regeln f\xfcr das Kyu Ranking.', verbose_name='Turnier')),
help_text='Diese Veranstaltung ist ein Turnier, es gelten andere Regeln f\xfcr das Kyu Ranking.',
verbose_name='Turnier')),
('photo_count', models.PositiveIntegerField(
default=0, editable=False)),
('event_series', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, editable=False, to='events.Event', blank=True,
help_text='Wenn dieser Termin zu einer Veranstaltungsreihe geh\xf6rt werden Ort, Beschreibung, Bild und Homepage von dem hier angegebenen Event \xfcbernommen.', null=True, verbose_name='Veranstaltungsreihen')),
('event_series',
models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL,
editable=False, to='events.Event',
blank=True,
help_text='Wenn dieser Termin zu einer Veranstaltungsreihe geh\xf6rt werden Ort, Beschreibung, Bild und Homepage von dem hier angegebenen Event \xfcbernommen.',
null=True,
verbose_name='Veranstaltungsreihen')),
],
options={
'ordering': ('-start', '-end'),
@@ -44,20 +52,310 @@ class Migration(migrations.Migration):
name='Location',
fields=[
('id', models.AutoField(verbose_name='ID',
serialize=False, auto_created=True, primary_key=True)),
serialize=False, auto_created=True,
primary_key=True)),
('name', models.CharField(max_length=200, verbose_name='Name')),
('description', models.TextField(
verbose_name='Beschreibung', blank=True)),
('image', models.ImageField(storage=utils.OverwriteStorage(
), upload_to=events.models.get_upload_path, null=True, verbose_name='Bild', blank=True)),
), upload_to=events.models.get_upload_path, null=True,
verbose_name='Bild', blank=True)),
('url', models.URLField(verbose_name='Homepage', blank=True)),
('postal_code', models.CharField(
max_length=6, verbose_name='Postleitzahl')),
('street_address', models.CharField(
max_length=127, verbose_name='Stra\xdfe')),
('locality', models.CharField(max_length=127, verbose_name='Ort')),
('country', models.CharField(max_length=2, verbose_name='Land', choices=[(b'GB', 'Vereinigtes K\xf6nigreich'), (b'AF', 'Afghanistan'), (b'AX', 'Aland Islands'), (b'AL', 'Albanien'), (b'DZ', 'Algerien'), (b'AS', 'Amerikanisch-Samoa'), (b'AD', 'Andorra'), (b'AO', 'Angola'), (b'AI', 'Anguilla'), (b'AQ', 'Antarktika'), (b'AG', 'Antigua und Barbuda'), (b'AR', 'Argentinien'), (b'AM', 'Armenien'), (b'AW', 'Aruba'), (b'AU', 'Australien'), (b'AT', '\xd6sterreich'), (b'AZ', 'Aserbaidschan'), (b'BS', 'Bahamas'), (b'BH', 'Bahrein'), (b'BD', 'Bangladesch'), (b'BB', 'Barbados'), (b'BY', 'Wei\xdfrussland'), (b'BE', 'Belgien'), (b'BZ', 'Belize'), (b'BJ', 'Benin'), (b'BM', 'Bermuda'), (b'BT', 'Bhutan'), (b'BO', 'Bolivien'), (b'BA', 'Bosnien und Herzegowina'), (b'BW', 'Botswana'), (b'BV', 'Bouvet Island'), (b'BR', 'Brasilien'), (b'IO', 'British Indian Ocean Territory'), (b'BN', 'Brunei Darussalam'), (b'BG', 'Bulgarien'), (b'BF', 'Burkina Faso'), (b'BI', 'Burundi'), (b'KH', 'Kambodscha'), (b'CM', 'Kamerun'), (b'CA', 'Kanada'), (b'CV', 'Cape Verde'), (b'KY', 'Cayman Islands'), (b'CF', 'Zentralafrikanische Republik'), (b'TD', 'Tschad'), (b'CL', 'Chile'), (b'CN', 'China'), (b'CX', 'Christmas Island'), (b'CC', 'Cocos (Keeling) Islands'), (b'CO', 'Kolumbien'), (b'KM', 'Komoren'), (b'CG', 'Kongo'), (b'CD', 'Kongo, Demokratische Republik'), (b'CK', 'Cook-Inseln'), (b'CR', 'Costa Rica'), (b'CI', "Cote d'Ivoire"), (b'HR', 'Kroatien'), (b'CU', 'Kuba'), (b'CY', 'Zypern'), (b'CZ', 'Tschechische Republik'), (b'DK', 'D\xe4nemark'), (b'DJ', 'Dschibuti'), (b'DM', 'Dominica'), (b'DO', 'Dominikanische Republik'), (b'EC', 'Ecuador'), (b'EG', '\xc4gypten'), (b'SV', 'El Salvador'), (b'GQ', '\xc4quatorial-Guinea'), (b'ER', 'Eritrea'), (b'EE', 'Estland'), (b'ET', '\xc4thiopien'), (b'FK', 'Falklandinseln (Malvinas)'), (b'FO', 'F\xe4r\xf6er-Inseln'), (b'FJ', 'Fidschi'), (b'FI', 'Finnland'), (b'FR', 'Frankreich'), (b'GF', 'Franz\xf6sisch-Guayana'), (b'PF', 'Franz\xf6sisch-Polynesien'), (b'TF', 'Franz\xf6sisch S\xfcdliche Territorien'), (b'GA', 'Gabun'), (b'GM', 'Gambia'), (b'GE', 'Georgia'), (b'DE', 'Deutschland'), (b'GH', 'Ghana'), (b'GI', 'Gibraltar'), (b'GR', 'Griechenland'), (b'GL', 'Gr\xf6nland'), (b'GD', 'Grenada'), (b'GP', 'Guadeloupe'), (b'GU', 'Guam'), (b'GT', 'Guatemala'), (b'GG', 'Guernsey'), (b'GN', 'Guinea'), (b'GW', 'Guinea-Bissau'), (b'GY', 'Guyana'), (b'HT', 'Haiti'), (b'HM', 'Heard und McDonald Inseln'), (b'VA', 'Heiliger Stuhl (Vatikanstadt)'), (b'HN', 'Honduras'), (b'HK', 'Hongkong'), (b'HU', 'Ungarn'), (b'IS', 'Island'), (b'IN', 'Indien'), (b'ID', 'Indonesien'), (b'IR', 'Iran, Islamische Republik'), (b'IQ', 'Irak'), (b'IE', 'Irland'), (b'IM', 'Isle of Man'), (b'IL', 'Israel'), (b'IT', 'Italien'), (b'JM', 'Jamaika'), (b'JP', 'Japan'), (b'JE', 'Jersey'), (b'JO', 'Jordan'), (b'KZ', 'Kasachstan'), (b'KE', 'Kenia'), (b'KI', 'Kiribati'), (b'KP', 'Korea, Demokratische Volksrepublik'), (b'KR', 'Korea, Republik'), (b'KW', 'Kuwait'), (b'KG', 'Kirgisistan'), (b'LA', 'Lao Demokratischen Volksrepublik'), (b'LV', 'Lettland'), (b'LB', 'Libanon'), (
b'LS', 'Lesotho'), (b'LR', 'Liberia'), (b'LY', 'Libyen'), (b'LI', 'Liechtenstein'), (b'LT', 'Litauen'), (b'LU', 'Luxemburg'), (b'MO', 'Macao'), (b'MK', 'Mazedonien, die ehemalige jugoslawische Republik'), (b'MG', 'Madagaskar'), (b'MW', 'Malawi'), (b'MY', 'Malaysia'), (b'MV', 'Malediven'), (b'ML', 'Mali'), (b'MT', 'Malta'), (b'MH', 'Marshall Islands'), (b'MQ', 'Martinique'), (b'MR', 'Mauretanien'), (b'MU', 'Mauritius'), (b'YT', 'Mayotte'), (b'MX', 'Mexiko'), (b'FM', 'Mikronesien, F\xf6derierte Staaten von'), (b'MD', 'Moldawien'), (b'MC', 'Monaco'), (b'MN', 'Mongolei'), (b'ME', 'Montenegro'), (b'MS', 'Montserrat'), (b'MA', 'Marokko'), (b'MZ', 'Mosambik'), (b'MM', 'Myanmar'), (b'NA', 'Namibia'), (b'NR', 'Nauru'), (b'NP', 'Nepal'), (b'NL', 'Niederlande'), (b'AN', 'Niederl\xe4ndische Antillen'), (b'NC', 'Neukaledonien'), (b'NZ', 'New Zealand'), (b'NI', 'Nicaragua'), (b'NE', 'Niger'), (b'NG', 'Nigeria'), (b'NU', 'Niue'), (b'NF', 'Norfolk Island'), (b'MP', 'Northern Mariana Islands'), (b'NO', 'Norwegen'), (b'OM', 'Oman'), (b'PK', 'Pakistan'), (b'PW', 'Palau'), (b'PS', 'Pal\xe4stinensische Autonomiegebiete'), (b'PA', 'Panama'), (b'PG', 'Papua-Neuguinea'), (b'PY', 'Paraguay'), (b'PE', 'Peru'), (b'PH', 'Philippinen'), (b'PN', 'Pitcairn'), (b'PL', 'Polen'), (b'PT', 'Portugal'), (b'PR', 'Puerto Rico'), (b'QA', 'Katar'), (b'RE', 'Wiedervereinigung'), (b'RO', 'Rum\xe4nien'), (b'RU', 'Russischen F\xf6deration'), (b'RW', 'Ruanda'), (b'BL', 'Saint Barthelemy'), (b'SH', 'Saint Helena'), (b'KN', 'Saint Kitts und Nevis'), (b'LC', 'Santa Lucia'), (b'MF', 'Santa Martin'), (b'PM', 'Saint Pierre und Miquelon'), (b'VC', 'Saint Vincent und die Grenadinen'), (b'WS', 'Samoa'), (b'SM', 'San Marino'), (b'ST', 'Sao Tome und Principe'), (b'SA', 'Saudi-Arabien'), (b'SN', 'Senegal'), (b'RS', 'Serbien'), (b'SC', 'Seychellen'), (b'SL', 'Sierra Leone'), (b'SG', 'Singapur'), (b'SK', 'Slowakei'), (b'SI', 'Slowenien'), (b'SB', 'Salomon-Inseln'), (b'SO', 'Somalia'), (b'ZA', 'S\xfcdafrika'), (b'GS', 'S\xfcdgeorgien und die S\xfcdlichen Sandwichinseln'), (b'ES', 'Spanien'), (b'LK', 'Sri Lanka'), (b'SD', 'Sudan'), (b'SR', 'Suriname'), (b'SJ', 'Svalbard und Jan Mayen'), (b'SZ', 'Swaziland'), (b'SE', 'Schweden'), (b'CH', 'Schweiz'), (b'SY', 'Arabische Republik Syrien'), (b'TW', 'Taiwan, Province of China'), (b'TJ', 'Tadschikistan'), (b'TZ', 'Tansania, Vereinigte Republik'), (b'TH', 'Thailand'), (b'TL', 'Timor-Leste'), (b'TG', 'Togo'), (b'TK', 'Tokelau'), (b'TO', 'Tonga'), (b'TT', 'Trinidad und Tobago'), (b'TN', 'Tunesien'), (b'TR', 'T\xfcrkei'), (b'TM', 'Turkmenistan'), (b'TC', 'Turks-und Caicosinseln'), (b'TV', 'Tuvalu'), (b'UG', 'Uganda'), (b'UA', 'Ukraine'), (b'AE', 'Vereinigte Arabische Emirate'), (b'US', 'Vereinigte Staaten'), (b'UM', 'United States Minor Outlying Islands'), (b'UY', 'Uruguay'), (b'UZ', 'Usbekistan'), (b'VU', 'Vanuatu'), (b'VE', 'Venezuela'), (b'VN', 'Vietnam'), (b'VG', 'Virgin Islands, British'), (b'VI', 'Virgin Islands, US'), (b'WF', 'Wallis und Futuna'), (b'EH', 'Westsahara'), (b'YE', 'Jemen'), (b'ZM', 'Sambia'), (b'ZW', 'Zimbabwe')])),
('locality',
models.CharField(max_length=127, verbose_name='Ort')),
('country', models.CharField(max_length=2, verbose_name='Land',
choices=[(b'GB',
'Vereinigtes K\xf6nigreich'),
(b'AF', 'Afghanistan'),
(b'AX', 'Aland Islands'),
(b'AL', 'Albanien'),
(b'DZ', 'Algerien'), (
b'AS',
'Amerikanisch-Samoa'),
(b'AD', 'Andorra'),
(b'AO', 'Angola'),
(b'AI', 'Anguilla'),
(b'AQ', 'Antarktika'), (
b'AG',
'Antigua und Barbuda'),
(b'AR', 'Argentinien'),
(b'AM', 'Armenien'),
(b'AW', 'Aruba'),
(b'AU', 'Australien'),
(b'AT', '\xd6sterreich'),
(b'AZ', 'Aserbaidschan'),
(b'BS', 'Bahamas'),
(b'BH', 'Bahrein'),
(b'BD', 'Bangladesch'),
(b'BB', 'Barbados'), (
b'BY',
'Wei\xdfrussland'),
(b'BE', 'Belgien'),
(b'BZ', 'Belize'),
(b'BJ', 'Benin'),
(b'BM', 'Bermuda'),
(b'BT', 'Bhutan'),
(b'BO', 'Bolivien'), (
b'BA',
'Bosnien und Herzegowina'),
(b'BW', 'Botswana'),
(b'BV', 'Bouvet Island'),
(b'BR', 'Brasilien'), (
b'IO',
'British Indian Ocean Territory'),
(b'BN',
'Brunei Darussalam'),
(b'BG', 'Bulgarien'),
(b'BF', 'Burkina Faso'),
(b'BI', 'Burundi'),
(b'KH', 'Kambodscha'),
(b'CM', 'Kamerun'),
(b'CA', 'Kanada'),
(b'CV', 'Cape Verde'),
(b'KY', 'Cayman Islands'),
(b'CF',
'Zentralafrikanische Republik'),
(b'TD', 'Tschad'),
(b'CL', 'Chile'),
(b'CN', 'China'), (b'CX',
'Christmas Island'),
(b'CC',
'Cocos (Keeling) Islands'),
(b'CO', 'Kolumbien'),
(b'KM', 'Komoren'),
(b'CG', 'Kongo'), (b'CD',
'Kongo, Demokratische Republik'),
(b'CK', 'Cook-Inseln'),
(b'CR', 'Costa Rica'),
(b'CI', "Cote d'Ivoire"),
(b'HR', 'Kroatien'),
(b'CU', 'Kuba'),
(b'CY', 'Zypern'), (b'CZ',
'Tschechische Republik'),
(b'DK', 'D\xe4nemark'),
(b'DJ', 'Dschibuti'),
(b'DM', 'Dominica'), (
b'DO',
'Dominikanische Republik'),
(b'EC', 'Ecuador'),
(b'EG', '\xc4gypten'),
(b'SV', 'El Salvador'), (
b'GQ',
'\xc4quatorial-Guinea'),
(b'ER', 'Eritrea'),
(b'EE', 'Estland'),
(b'ET', '\xc4thiopien'), (
b'FK',
'Falklandinseln (Malvinas)'),
(b'FO',
'F\xe4r\xf6er-Inseln'),
(b'FJ', 'Fidschi'),
(b'FI', 'Finnland'),
(b'FR', 'Frankreich'), (
b'GF',
'Franz\xf6sisch-Guayana'),
(b'PF',
'Franz\xf6sisch-Polynesien'),
(b'TF',
'Franz\xf6sisch S\xfcdliche Territorien'),
(b'GA', 'Gabun'),
(b'GM', 'Gambia'),
(b'GE', 'Georgia'),
(b'DE', 'Deutschland'),
(b'GH', 'Ghana'),
(b'GI', 'Gibraltar'),
(b'GR', 'Griechenland'),
(b'GL', 'Gr\xf6nland'),
(b'GD', 'Grenada'),
(b'GP', 'Guadeloupe'),
(b'GU', 'Guam'),
(b'GT', 'Guatemala'),
(b'GG', 'Guernsey'),
(b'GN', 'Guinea'),
(b'GW', 'Guinea-Bissau'),
(b'GY', 'Guyana'),
(b'HT', 'Haiti'), (b'HM',
'Heard und McDonald Inseln'),
(b'VA',
'Heiliger Stuhl (Vatikanstadt)'),
(b'HN', 'Honduras'),
(b'HK', 'Hongkong'),
(b'HU', 'Ungarn'),
(b'IS', 'Island'),
(b'IN', 'Indien'),
(b'ID', 'Indonesien'), (
b'IR',
'Iran, Islamische Republik'),
(b'IQ', 'Irak'),
(b'IE', 'Irland'),
(b'IM', 'Isle of Man'),
(b'IL', 'Israel'),
(b'IT', 'Italien'),
(b'JM', 'Jamaika'),
(b'JP', 'Japan'),
(b'JE', 'Jersey'),
(b'JO', 'Jordan'),
(b'KZ', 'Kasachstan'),
(b'KE', 'Kenia'),
(b'KI', 'Kiribati'), (
b'KP',
'Korea, Demokratische Volksrepublik'),
(
b'KR',
'Korea, Republik'),
(b'KW', 'Kuwait'),
(b'KG', 'Kirgisistan'), (
b'LA',
'Lao Demokratischen Volksrepublik'),
(b'LV', 'Lettland'),
(b'LB', 'Libanon'), (
b'LS', 'Lesotho'),
(b'LR', 'Liberia'),
(b'LY', 'Libyen'),
(b'LI', 'Liechtenstein'),
(b'LT', 'Litauen'),
(b'LU', 'Luxemburg'),
(b'MO', 'Macao'), (b'MK',
'Mazedonien, die ehemalige jugoslawische Republik'),
(b'MG', 'Madagaskar'),
(b'MW', 'Malawi'),
(b'MY', 'Malaysia'),
(b'MV', 'Malediven'),
(b'ML', 'Mali'),
(b'MT', 'Malta'), (b'MH',
'Marshall Islands'),
(b'MQ', 'Martinique'),
(b'MR', 'Mauretanien'),
(b'MU', 'Mauritius'),
(b'YT', 'Mayotte'),
(b'MX', 'Mexiko'), (b'FM',
'Mikronesien, F\xf6derierte Staaten von'),
(b'MD', 'Moldawien'),
(b'MC', 'Monaco'),
(b'MN', 'Mongolei'),
(b'ME', 'Montenegro'),
(b'MS', 'Montserrat'),
(b'MA', 'Marokko'),
(b'MZ', 'Mosambik'),
(b'MM', 'Myanmar'),
(b'NA', 'Namibia'),
(b'NR', 'Nauru'),
(b'NP', 'Nepal'),
(b'NL', 'Niederlande'), (
b'AN',
'Niederl\xe4ndische Antillen'),
(b'NC', 'Neukaledonien'),
(b'NZ', 'New Zealand'),
(b'NI', 'Nicaragua'),
(b'NE', 'Niger'),
(b'NG', 'Nigeria'),
(b'NU', 'Niue'),
(b'NF', 'Norfolk Island'),
(b'MP',
'Northern Mariana Islands'),
(b'NO', 'Norwegen'),
(b'OM', 'Oman'),
(b'PK', 'Pakistan'),
(b'PW', 'Palau'), (b'PS',
'Pal\xe4stinensische Autonomiegebiete'),
(b'PA', 'Panama'), (
b'PG',
'Papua-Neuguinea'),
(b'PY', 'Paraguay'),
(b'PE', 'Peru'),
(b'PH', 'Philippinen'),
(b'PN', 'Pitcairn'),
(b'PL', 'Polen'),
(b'PT', 'Portugal'),
(b'PR', 'Puerto Rico'),
(b'QA', 'Katar'), (b'RE',
'Wiedervereinigung'),
(b'RO', 'Rum\xe4nien'), (
b'RU',
'Russischen F\xf6deration'),
(b'RW', 'Ruanda'), (b'BL',
'Saint Barthelemy'),
(b'SH', 'Saint Helena'), (
b'KN',
'Saint Kitts und Nevis'),
(b'LC', 'Santa Lucia'),
(b'MF', 'Santa Martin'), (
b'PM',
'Saint Pierre und Miquelon'),
(b'VC',
'Saint Vincent und die Grenadinen'),
(b'WS', 'Samoa'),
(b'SM', 'San Marino'), (
b'ST',
'Sao Tome und Principe'),
(b'SA', 'Saudi-Arabien'),
(b'SN', 'Senegal'),
(b'RS', 'Serbien'),
(b'SC', 'Seychellen'),
(b'SL', 'Sierra Leone'),
(b'SG', 'Singapur'),
(b'SK', 'Slowakei'),
(b'SI', 'Slowenien'),
(b'SB', 'Salomon-Inseln'),
(b'SO', 'Somalia'),
(b'ZA', 'S\xfcdafrika'), (
b'GS',
'S\xfcdgeorgien und die S\xfcdlichen Sandwichinseln'),
(b'ES', 'Spanien'),
(b'LK', 'Sri Lanka'),
(b'SD', 'Sudan'),
(b'SR', 'Suriname'), (
b'SJ',
'Svalbard und Jan Mayen'),
(b'SZ', 'Swaziland'),
(b'SE', 'Schweden'),
(b'CH', 'Schweiz'), (
b'SY',
'Arabische Republik Syrien'),
(b'TW',
'Taiwan, Province of China'),
(b'TJ', 'Tadschikistan'),
(b'TZ',
'Tansania, Vereinigte Republik'),
(b'TH', 'Thailand'),
(b'TL', 'Timor-Leste'),
(b'TG', 'Togo'),
(b'TK', 'Tokelau'),
(b'TO', 'Tonga'), (b'TT',
'Trinidad und Tobago'),
(b'TN', 'Tunesien'),
(b'TR', 'T\xfcrkei'),
(b'TM', 'Turkmenistan'), (
b'TC',
'Turks-und Caicosinseln'),
(b'TV', 'Tuvalu'),
(b'UG', 'Uganda'),
(b'UA', 'Ukraine'), (
b'AE',
'Vereinigte Arabische Emirate'),
(b'US',
'Vereinigte Staaten'), (
b'UM',
'United States Minor Outlying Islands'),
(b'UY', 'Uruguay'),
(b'UZ', 'Usbekistan'),
(b'VU', 'Vanuatu'),
(b'VE', 'Venezuela'),
(b'VN', 'Vietnam'), (
b'VG',
'Virgin Islands, British'),
(b'VI',
'Virgin Islands, US'), (
b'WF',
'Wallis und Futuna'),
(b'EH', 'Westsahara'),
(b'YE', 'Jemen'),
(b'ZM', 'Sambia'),
(b'ZW', 'Zimbabwe')])),
],
options={
'verbose_name': 'Veranstaltungsort',
@@ -67,6 +365,8 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='event',
name='location',
field=models.ForeignKey(to='events.Location'),
field=models.ForeignKey(
to='events.Location',
on_delete=models.CASCADE),
),
]

View File

@@ -1,17 +1,17 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import ckeditor.fields
import events.models
import easy_thumbnails.fields
import django.db.models.deletion
import utils
import easy_thumbnails.fields
from django.conf import settings
from django.db import models, migrations
import events.models
import utils
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('events', '0003_auto_20150823_2232'),
@@ -22,18 +22,24 @@ class Migration(migrations.Migration):
name='Photo',
fields=[
('id', models.AutoField(verbose_name='ID',
serialize=False, auto_created=True, primary_key=True)),
serialize=False, auto_created=True,
primary_key=True)),
('name', models.CharField(max_length=100,
verbose_name='Name', blank=True)),
('image', easy_thumbnails.fields.ThumbnailerImageField(
upload_to=events.models.get_upload_path, storage=utils.OverwriteStorage(), verbose_name='Bild')),
upload_to=events.models.get_upload_path,
storage=utils.OverwriteStorage(), verbose_name='Bild')),
('description', models.TextField(max_length=300,
verbose_name='Beschreibung', blank=True)),
verbose_name='Beschreibung',
blank=True)),
('on_startpage', models.BooleanField(default=False,
help_text='Display this Photo on the Startpage Teaser', verbose_name='Startpage')),
('created_date', models.DateTimeField(verbose_name='Published on')),
help_text='Display this Photo on the Startpage Teaser',
verbose_name='Startpage')),
('created_date',
models.DateTimeField(verbose_name='Published on')),
('views', models.PositiveIntegerField(default=0,
verbose_name='Number of views', editable=False)),
verbose_name='Number of views',
editable=False)),
],
options={
'ordering': ['created_date'],
@@ -46,7 +52,8 @@ class Migration(migrations.Migration):
migrations.AlterModelOptions(
name='event',
options={'ordering': (
'start', 'end'), 'verbose_name': 'Termin', 'verbose_name_plural': 'Termine'},
'start', 'end'), 'verbose_name': 'Termin',
'verbose_name_plural': 'Termine'},
),
migrations.AlterField(
model_name='event',
@@ -57,14 +64,19 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='event',
name='event_series',
field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, blank=True, to='events.Event',
help_text='Wenn dieser Termin zu einer Veranstaltungsreihe geh\xf6rt werden Ort, Beschreibung, Bild und Homepage von dem hier angegebenen Event \xfcbernommen.', null=True, verbose_name='Veranstaltungsreihen'),
field=models.ForeignKey(
on_delete=django.db.models.deletion.SET_NULL, blank=True,
to='events.Event',
help_text='Wenn dieser Termin zu einer Veranstaltungsreihe geh\xf6rt werden Ort, Beschreibung, Bild und Homepage von dem hier angegebenen Event \xfcbernommen.',
null=True, verbose_name='Veranstaltungsreihen'),
),
migrations.AlterField(
model_name='event',
name='image',
field=easy_thumbnails.fields.ThumbnailerImageField(storage=utils.OverwriteStorage(
), upload_to=events.models.get_upload_path, null=True, verbose_name='Bild', blank=True),
field=easy_thumbnails.fields.ThumbnailerImageField(
storage=utils.OverwriteStorage(
), upload_to=events.models.get_upload_path, null=True,
verbose_name='Bild', blank=True),
),
migrations.AlterField(
model_name='location',
@@ -75,17 +87,21 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='location',
name='image',
field=easy_thumbnails.fields.ThumbnailerImageField(storage=utils.OverwriteStorage(
), upload_to=events.models.get_upload_path, null=True, verbose_name='Bild', blank=True),
field=easy_thumbnails.fields.ThumbnailerImageField(
storage=utils.OverwriteStorage(
), upload_to=events.models.get_upload_path, null=True,
verbose_name='Bild', blank=True),
),
migrations.AddField(
model_name='photo',
name='event',
field=models.ForeignKey(to='events.Event'),
field=models.ForeignKey(
to='events.Event', on_delete=models.CASCADE),
),
migrations.AddField(
model_name='photo',
name='photographer',
field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
field=models.ForeignKey(
to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE),
),
]

View File

@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.8 on 2017-12-14 11:15
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('events', '0008_auto_20171115_0653'),
]
operations = [
migrations.AlterField(
model_name='event',
name='location',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='events.Location'),
),
migrations.AlterField(
model_name='photo',
name='event',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='events.Event'),
),
migrations.AlterField(
model_name='photo',
name='photographer',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
),
]

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,8 @@
"""Mixins for Events."""
from django.db.models import Q
from django.http import Http404
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext as _
from . import models
@@ -34,12 +37,14 @@ class EventDetailMixin(object):
:return: TemplateContext object"""
context = super(EventDetailMixin, self).get_context_data(**kwargs)
if hasattr(self, 'event') and self.event:
if hasattr(self, 'event'):
context['event'] = self.event
elif hasattr(self, 'object') and isinstance(self.object, models.Event):
context['event'] = self.object
elif hasattr(self, 'object') and hasattr(self.object, 'event'):
context['event'] = self.object.event
else:
print("No Event in Context!")
return context
def get_queryset(self):
@@ -49,10 +54,23 @@ class EventDetailMixin(object):
:return: a django QuerySets
"""
if self.model == models.Event:
return models.Event.objects.all()
try:
self.event = models.Event.objects.get(pk=self.kwargs['event'])
self.event = get_object_or_404(models.Event, pk=self.kwargs['pk'])
queryset = self.model.objects.all()
else:
self.event = get_object_or_404(models.Event,
pk=self.kwargs['event'])
queryset = self.model.objects.filter(event=self.event)
return queryset.prefetch_related()
class EventPhotoMixin(EventDetailMixin):
def get_queryset(self):
try:
self.event = models.Event.objects.get(id=self.kwargs['event'])
return models.Photo.objects.filter(
Q(event=self.event) |
Q(event__event_series=self.event)
)
except models.Event.DoesNotExist:
raise Http404(_('Event does not exist'))
return queryset.prefetch_related()

View File

@@ -4,10 +4,10 @@ import os
from ckeditor.fields import RichTextField
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.db import models
from django.db.models import Q
from django.template.defaultfilters import slugify
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import ugettext as _
from easy_thumbnails.fields import ThumbnailerImageField
@@ -51,7 +51,7 @@ class Event(models.Model):
"""An Event that could be a tournament, a game session, or an convention."""
name = models.CharField(_('Name'), max_length=255)
description = RichTextField(_("Description"), blank=True)
location = models.ForeignKey('Location')
location = models.ForeignKey('Location', on_delete=models.PROTECT)
start = models.DateTimeField(_('Start'))
end = models.DateTimeField(_('End'), blank=True, null=True)
url = models.URLField(_('Homepage'), blank=True)
@@ -220,13 +220,14 @@ class Photo(models.Model):
upload_to=get_upload_path,
storage=OverwriteStorage()
)
event = models.ForeignKey('events.Event')
event = models.ForeignKey('events.Event', on_delete=models.PROTECT, )
description = models.TextField(
_("Description"),
max_length=300,
blank=True
)
photographer = models.ForeignKey(settings.AUTH_USER_MODEL)
photographer = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.PROTECT)
on_startpage = models.BooleanField(
_("Startpage"),
default=False,

View File

@@ -40,7 +40,7 @@
<h4><a href="{{ event.get_absolute_url }}">{{ event.name }}</a></h4>
<p>
<span class="fa fa-calendar-o" title="{% trans 'Date' %}" aria-label="{% trans 'Date' %}"></span>
<time datetime="{{event.start|date:'c'}}">
<time datetime="{{event.start|date:'Y-m-d\TH:i:sO'}}">
{{ event.start|date:'D' }}
{{ event.start|date:'SHORT_DATE_FORMAT' }} {{hanchan.start|time:'H:i'}}
</time>
@@ -52,7 +52,7 @@
{% endif %}
</p>
<p>{{event.description|truncatewords_html:20|safe}}</p>
{{event.description|truncatewords_html:20|safe}}
<ul class="info">
<li><span class="fa fa-map-marker" title="{% trans 'Location' %}"></span> {{ event.location }}</li>
<li><span class="fa fa-comments" title="{% trans 'Comments' %}"></span> <a href="{{event.get_absolute_url}}#comments">{{ comment_count }}</a></li>

View File

@@ -20,7 +20,7 @@
<h4><a href="{{ event.get_absolute_url }}">{{ event.name }}</a></h4>
<ul class="info">
<li><span class="fa fa-calendar-o" title="{% trans 'Date' %}" aria-label="{% trans 'Date' %}"></span>
<time datetime="{{event.start|date:'c'}}">
<time datetime="{{event.start|date:'Y-m-d\TH:i:sO'}}">
{{ event.start|date:'D' }}
{{ event.start|date:'SHORT_DATE_FORMAT' }}
</time>
@@ -36,10 +36,9 @@
<a href="{{event.get_absolute_url}}#comments">{{ comment_count }}</a>
</li>
</ul>
<p>{{event.description|truncatewords_html:25|safe}}</p>
{{event.description|truncatewords_html:25|safe}}
</div>
{% if forloop.counter|divisibleby:2 %}<br class="clear">{% endif %}
{% endfor %}
{% endfor %}
{% if page_obj.has_other_pages %}{% include 'paginator.html' %}{% endif %}
{% endblock %}

View File

@@ -3,9 +3,10 @@ from datetime import timedelta
from django.contrib.auth import get_user_model
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.urlresolvers import reverse
from django.urls import reverse
from django.db.models import Q
from django.http import HttpResponse, Http404
from django.http import Http404
from django.http import HttpResponse
from django.shortcuts import redirect
from django.utils import timezone
from django.utils.translation import ugettext as _
@@ -75,14 +76,17 @@ class EventForm(PermissionRequiredMixin, mixins.EventDetailMixin,
class EventGallery(generic.ListView):
"""Display a overview of all event photo albums."""
template_name = 'events/photo_gallery.html'
queryset = models.Event.objects.filter(
start__lt=timezone.now(),
event_series__isnull=True,
photo_count__gt=0
)
queryset = queryset.order_by('-start')
paginate_by = 24
def get_queryset(self):
queryset = models.Event.objects.filter(
start__lt=timezone.now(),
event_series__isnull=True,
photo_count__gt=0
)
queryset = queryset.order_by('-start')
return queryset
class EventListIcal(generic.View):
"""Generates an returns an iCal File with all upcoming events."""
@@ -110,7 +114,7 @@ class EventListIcal(generic.View):
return response
class EventPhoto(mixins.EventDetailMixin, generic.UpdateView):
class EventPhoto(mixins.EventPhotoMixin, generic.UpdateView):
"""Display the requested Photo and allows rotation if the user has change
permissions."""
form_class = forms.EditPhotoForm
@@ -127,7 +131,7 @@ class EventPhoto(mixins.EventDetailMixin, generic.UpdateView):
return generic.UpdateView.post(self, request, *args, **kwargs)
class EventPhotoList(mixins.EventDetailMixin, generic.ListView):
class EventPhotoList(mixins.EventPhotoMixin, generic.ListView):
"""List all Photos of the event or event series in an album."""
context_object_name = 'photo_list'
event = None
@@ -139,16 +143,6 @@ class EventPhotoList(mixins.EventDetailMixin, generic.ListView):
initial={'event': self.event, 'photographer': self.request.user})
return context
def get_queryset(self):
try:
self.event = models.Event.objects.get(id=self.kwargs['event'])
return models.Photo.objects.filter(
Q(event=self.event) |
Q(event__event_series=self.event)
)
except models.Event.DoesNotExist:
raise Http404(_('Event does not exist'))
class EventPhotoUpload(mixins.EventDetailMixin, generic.FormView):
form_class = forms.PhotoUploadForm
@@ -187,10 +181,9 @@ class EventSeriesForm(mixins.EventDetailMixin, PermissionRequiredMixin,
InlineFormSetView):
model = models.Event
inline_model = models.Event
fk_name = 'event_series'
fields = ('start', 'end')
form_class = forms.EventForm
extra = 3
factory_kwargs = {'extra': 3, 'fk_name': 'event_series'}
permission_required = 'events.add_event'
template_name = 'events/eventseries_form.html'
@@ -202,5 +195,7 @@ class EventSeriesForm(mixins.EventDetailMixin, PermissionRequiredMixin,
class UpcomingEvents(generic.ListView):
queryset = models.Event.objects.upcoming(limit=None)
paginate_by = 16
def get_queryset(self):
return models.Event.objects.upcoming(limit=None)

View File

@@ -7,9 +7,9 @@ msgid ""
msgstr ""
"Project-Id-Version: kasu.utils\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-04-27 09:49+0200\n"
"PO-Revision-Date: 2016-09-28 00:24+0200\n"
"Last-Translator: Christian Berg <xeniac.at@gmail.com>\n"
"POT-Creation-Date: 2019-12-13 23:38+0100\n"
"PO-Revision-Date: 2018-12-30 11:14+0105\n"
"Last-Translator: b' <kasu@xendynastie.at>'\n"
"Language-Team: Kasu <verein@kasu.at>\n"
"Language: de\n"
"MIME-Version: 1.0\n"
@@ -17,13 +17,13 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 1.8.9\n"
"X-Translated-Using: django-rosetta 0.7.6\n"
"X-Translated-Using: django-rosetta 0.9.0\n"
#: settings.py:157
#: settings.py:140
msgid "German"
msgstr "Deutsch"
#: settings.py:157
#: settings.py:140
msgid "English"
msgstr "Englisch"
@@ -43,101 +43,90 @@ msgstr "Kürzliche Kommentare"
msgid "Menu"
msgstr "Menü"
#: templates/base.html:69 templates/redbox.html:3
#: templates/base.html:69
msgid "Current Event"
msgstr "Aktuelle Veranstaltung"
#: templates/base.html:72 templates/redbox.html:6
#: templates/base.html:72
msgid "Since"
msgstr "seit"
#: templates/base.html:73 templates/base.html:85 templates/redbox.html:8
#: templates/redbox.html:22
#: templates/base.html:73 templates/base.html:87
msgid "Start"
msgstr "Beginn"
#: templates/base.html:76 templates/base.html:88 templates/redbox.html:11
#: templates/redbox.html:25
#: templates/base.html:76 templates/base.html:90
msgid "Location"
msgstr "Ort"
#: templates/base.html:79 templates/base.html:90 templates/redbox.html:14
#: templates/redbox.html:28
#: templates/base.html:81 templates/base.html:94
msgid "More Details"
msgstr "Mehr Details"
#: templates/base.html:81 templates/redbox.html:17
#: templates/base.html:83
msgid "Next Event"
msgstr "Nächste Veranstaltung"
#: templates/base.html:84 templates/redbox.html:20
#: templates/base.html:86
msgid "in"
msgstr "in"
#: templates/base.html:93 templates/redbox.html:30
#: templates/base.html:97
msgid "Upcoming events"
msgstr "Bevorstehende Veranstaltungen"
#: templates/base.html:143
#: templates/base.html:107
msgid "No events found"
msgstr "Keine Veranstaltungen gefunden"
#: templates/base.html:149
msgid "Add Subpage"
msgstr "Unterseite Hinzufügen"
#: templates/base.html:148
#: templates/base.html:154
msgid "Edit Page"
msgstr "Seite bearbeiten"
#: templates/base.html:156
#: templates/base.html:162
msgid "Imprint"
msgstr "Impressum"
#: templates/base.html:157
#: templates/base.html:163
msgid "contact"
msgstr "Kontakt"
#: templates/base.html:162
#: templates/base.html:168
msgid "Language"
msgstr "Sprache"
#: templates/base.html:171
#: templates/base.html:177
msgid "Go"
msgstr "Los"
#: templates/base.html:176
#: templates/base.html:182
msgid "Logged in as"
msgstr "Angemeldet als"
#: templates/base.html:178
#: templates/base.html:184
msgid "Admin"
msgstr "Admin"
#: templates/base.html:179
#: templates/base.html:185
msgid "Logout"
msgstr "Abmelden"
#: templates/base.html:181
#: templates/base.html:187
msgid "no user logged in"
msgstr "Niemand angemeldet"
#: templates/base.html:182 templates/comments/form.html:43
#: templates/base.html:188 templates/comments/form.html:43
msgid "register"
msgstr "Registrieren"
#: templates/base.html:183 templates/comments/form.html:44
#: templates/base.html:189 templates/comments/form.html:44
msgid "login"
msgstr "anmelden"
#: templates/base.html:185
msgid "Login with Facebook"
msgstr "über Facebook anmelden"
#: templates/base.html:187
msgid "Login with Twitter"
msgstr "über Twitter anmelden"
#: templates/base.html:189
msgid "Login with Google"
msgstr "über Google anmelden"
#: templates/comments/form.html:5
msgid "New Comment"
msgstr "Neuer Kommentar"
@@ -189,13 +178,22 @@ msgid "Read More"
msgstr "Mehr lesen"
#: templates/index.html:47
#, python-format
#, fuzzy, python-format
#| msgid ""
#| "\n"
#| " From <a href=\"%(user_link)s\">%(author)s</a> in\n"
#| " <a href=\"%(comment_link)s\">&ldquo;%(object)s&rdquo;</"
#| "a>\n"
#| " since\n"
#| " <time datetime=\"%(submit_date)s\">%(since)s</time>\n"
#| " "
msgid ""
"\n"
" From <a href=\"%(user_link)s\">%(author)s</a> in\n"
" <a href=\"%(comment_link)s\">&ldquo;%(object)s&rdquo;</a>\n"
" since\n"
" <time datetime=\"%(submit_date)s\">%(since)s</time>\n"
" <time datetime=\"%(submit_date|date:'Y-m-d\\TH:i:sO')s\">"
"%(since)s</time>\n"
" "
msgstr ""
"\n"
@@ -225,6 +223,15 @@ msgstr "Vorherige"
msgid "Next"
msgstr "Nächste"
#~ msgid "Login with Facebook"
#~ msgstr "über Facebook anmelden"
#~ msgid "Login with Twitter"
#~ msgstr "über Twitter anmelden"
#~ msgid "Login with Google"
#~ msgstr "über Google anmelden"
#~ msgid "United Kingdom"
#~ msgstr "Vereinigtes Königreich"

View File

@@ -49,7 +49,6 @@ PREREQ_APPS = [
'ckeditor',
'ckeditor_uploader',
'easy_thumbnails',
'social_django',
]
PROJECT_APPS = [
'kasu',
@@ -69,17 +68,17 @@ CACHES = {
}
# Request Middleware
MIDDLEWARE_CLASSES = [
MIDDLEWARE = [
'csp.middleware.CSPMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.locale.LocaleMiddleware',
'utils.middleware.SetRemoteAddrFromForwardedFor',
'mahjong_ranking.middleware.DenormalizationUpdateMiddleware',
]
@@ -102,8 +101,6 @@ TEMPLATES = [
'django.contrib.messages.context_processors.messages',
'django.contrib.messages.context_processors.messages',
'events.context_processors.events_overview',
'social_django.context_processors.backends',
'social_django.context_processors.login_redirect'
],
'loaders': [
('django.template.loaders.cached.Loader', [
@@ -131,24 +128,10 @@ DEFAULT_FROM_EMAIL = ""
# Login Settings
ACCOUNT_ACTIVATION_DAYS = 5
AUTH_USER_MODEL = 'membership.Membership'
AUTHENTICATION_BACKENDS = ('social_core.backends.facebook.FacebookOAuth2',
'social_core.backends.google.GoogleOAuth2',
'social_core.backends.twitter.TwitterOAuth',
'django.contrib.auth.backends.ModelBackend',)
AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',)
LOGIN_URL = '/membership/login/'
LOGIN_ERROR_URL = '/membership/login/error/'
LOGIN_REDIRECT_URL = '/users/'
SOCIAL_AUTH_CHANGE_SIGNAL_ONLY = False
SOCIAL_AUTH_ENABLED_BACKENDS = ('facebook-oauth2', 'google-oauth2', 'twitter')
SOCIAL_AUTH_NEW_USER_REDIRECT_URL = '/users/'
SOCIAL_AUTH_SLUGIFY_USERNAMES = True
SOCIAL_AUTH_FACEBOOK_KEY = ''
SOCIAL_AUTH_FACEBOOK_SECRET = ''
SOCIAL_AUTH_FACEBOOK_SCOPE = ['user_about_me', 'email']
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = ''
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = ''
SOCIAL_AUTH_TWITTER_KEY = ''
SOCIAL_AUTH_TWITTER_SECRET = ''
# Localization
USE_I18N = True
@@ -229,7 +212,7 @@ LOGGING = {
'loggers': {
'django': {
'handlers': ['console'],
'level': 'INFO',
'level': 'DEBUG',
'propagate': True,
},
'django.request': {
@@ -246,6 +229,22 @@ LOGGING = {
}
}
################################
# Settings for mahjong_ranking #
################################
MIN_HANCHANS_FOR_LADDER = 5
RANKING_EXPORT_PATH = path.join(PROJECT_PATH, 'backup', 'mahjong_ranking')
# Old Tournament System
TOURNAMENT_POINT_SYSTEM = True
TOURNAMENT_WIN_BONUSPOINTS = 4
TOURNAMENT_FLAWLESS_VICTORY_BONUSPOINTS = 8
# Old Dan System
DAN_3_WINS_IN_A_ROW = True
DAN_ALLOW_DROP_DOWN = True
DAN_RANKS = (
(80, 9),
(70, 8),
@@ -255,7 +254,7 @@ DAN_RANKS = (
(30, 4),
(20, 3),
(10, 2),
(0, 1),
(-1, 1),
)
KYU_RANKS = (
@@ -268,13 +267,9 @@ KYU_RANKS = (
(15, 7),
(10, 8),
(5, 9),
(0, 10),
(-1, 10),
)
DAN_ALLOW_DROP_DOWN = True
MIN_HANCHANS_FOR_LADDER = 5
RANKING_EXPORT_PATH = path.join(PROJECT_PATH, 'backup', 'mahjong_ranking')
try:
from .local_settings import * # Ignore PyLintBear (W0401, W0614)
except ImportError:

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,231 @@
/**
* jQuery Formset 1.3-pre
* @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com)
* @requires jQuery 1.2.6 or later
*
* Copyright (c) 2009, Stanislaus Madueke
* All rights reserved.
*
* Licensed under the New BSD License
* See: http://www.opensource.org/licenses/bsd-license.php
*/
;(function($) {
$.fn.formset = function(opts)
{
var options = $.extend({}, $.fn.formset.defaults, opts),
flatExtraClasses = options.extraClasses.join(' '),
totalForms = $('#id_' + options.prefix + '-TOTAL_FORMS'),
maxForms = $('#id_' + options.prefix + '-MAX_NUM_FORMS'),
minForms = $('#id_' + options.prefix + '-MIN_NUM_FORMS'),
childElementSelector = 'input,select,textarea,label,div',
$$ = $(this),
applyExtraClasses = function(row, ndx) {
if (options.extraClasses) {
row.removeClass(flatExtraClasses);
row.addClass(options.extraClasses[ndx % options.extraClasses.length]);
}
},
updateElementIndex = function(elem, prefix, ndx) {
var idRegex = new RegExp(prefix + '-(\\d+|__prefix__)-'),
replacement = prefix + '-' + ndx + '-';
if (elem.attr("for")) elem.attr("for", elem.attr("for").replace(idRegex, replacement));
if (elem.attr('id')) elem.attr('id', elem.attr('id').replace(idRegex, replacement));
if (elem.attr('name')) elem.attr('name', elem.attr('name').replace(idRegex, replacement));
},
hasChildElements = function(row) {
return row.find(childElementSelector).length > 0;
},
showAddButton = function() {
return maxForms.length == 0 || // For Django versions pre 1.2
(maxForms.val() == '' || (maxForms.val() - totalForms.val() > 0));
},
/**
* Indicates whether delete link(s) can be displayed - when total forms > min forms
*/
showDeleteLinks = function() {
return minForms.length == 0 || // For Django versions pre 1.7
(minForms.val() == '' || (totalForms.val() - minForms.val() > 0));
},
insertDeleteLink = function(row) {
var delCssSelector = $.trim(options.deleteCssClass).replace(/\s+/g, '.'),
addCssSelector = $.trim(options.addCssClass).replace(/\s+/g, '.');
if (row.is('TR')) {
// If the forms are laid out in table rows, insert
// the remove button into the last table cell:
row.children(':last').append('<a class="' + options.deleteCssClass +'" href="javascript:void(0)">' + options.deleteText + '</a>');
} else if (row.is('UL') || row.is('OL')) {
// If they're laid out as an ordered/unordered list,
// insert an <li> after the last list item:
row.append('<li><a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText +'</a></li>');
} else {
// Otherwise, just insert the remove button as the
// last child element of the form's container:
row.append('<a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText +'</a>');
}
// Check if we're under the minimum number of forms - not to display delete link at rendering
if (!showDeleteLinks()){
row.find('a.' + delCssSelector).hide();
}
row.find('a.' + delCssSelector).click(function() {
var row = $(this).parents('.' + options.formCssClass),
del = row.find('input:hidden[id $= "-DELETE"]'),
buttonRow = row.siblings("a." + addCssSelector + ', .' + options.formCssClass + '-add'),
forms;
if (del.length) {
// We're dealing with an inline formset.
// Rather than remove this form from the DOM, we'll mark it as deleted
// and hide it, then let Django handle the deleting:
del.val('on');
row.hide();
forms = $('.' + options.formCssClass).not(':hidden');
} else {
row.remove();
// Update the TOTAL_FORMS count:
forms = $('.' + options.formCssClass).not('.formset-custom-template');
totalForms.val(forms.length);
}
for (var i=0, formCount=forms.length; i<formCount; i++) {
// Apply `extraClasses` to form rows so they're nicely alternating:
applyExtraClasses(forms.eq(i), i);
if (!del.length) {
// Also update names and IDs for all child controls (if this isn't
// a delete-able inline formset) so they remain in sequence:
forms.eq(i).find(childElementSelector).each(function() {
updateElementIndex($(this), options.prefix, i);
});
}
}
// Check if we've reached the minimum number of forms - hide all delete link(s)
if (!showDeleteLinks()){
$('a.' + delCssSelector).each(function(){$(this).hide();});
}
// Check if we need to show the add button:
if (buttonRow.is(':hidden') && showAddButton()) buttonRow.show();
// If a post-delete callback was provided, call it with the deleted form:
if (options.removed) options.removed(row);
return false;
});
};
$$.each(function(i) {
var row = $(this),
del = row.find('input:checkbox[id $= "-DELETE"]');
if (del.length) {
// If you specify "can_delete = True" when creating an inline formset,
// Django adds a checkbox to each form in the formset.
// Replace the default checkbox with a hidden field:
if (del.is(':checked')) {
// If an inline formset containing deleted forms fails validation, make sure
// we keep the forms hidden (thanks for the bug report and suggested fix Mike)
del.before('<input type="hidden" name="' + del.attr('name') +'" id="' + del.attr('id') +'" value="on" />');
row.hide();
} else {
del.before('<input type="hidden" name="' + del.attr('name') +'" id="' + del.attr('id') +'" />');
}
// Hide any labels associated with the DELETE checkbox:
$('label[for="' + del.attr('id') + '"]').hide();
del.remove();
}
if (hasChildElements(row)) {
row.addClass(options.formCssClass);
if (row.is(':visible')) {
insertDeleteLink(row);
applyExtraClasses(row, i);
}
}
});
if ($$.length) {
var hideAddButton = !showAddButton(),
addButton, template;
if (options.formTemplate) {
// If a form template was specified, we'll clone it to generate new form instances:
template = (options.formTemplate instanceof $) ? options.formTemplate : $(options.formTemplate);
template.removeAttr('id').addClass(options.formCssClass + ' formset-custom-template');
template.find(childElementSelector).each(function() {
updateElementIndex($(this), options.prefix, '__prefix__');
});
insertDeleteLink(template);
} else {
// Otherwise, use the last form in the formset; this works much better if you've got
// extra (>= 1) forms (thnaks to justhamade for pointing this out):
template = $('.' + options.formCssClass + ':last').clone(true).removeAttr('id');
template.find('input:hidden[id $= "-DELETE"]').remove();
// Clear all cloned fields, except those the user wants to keep (thanks to brunogola for the suggestion):
template.find(childElementSelector).not(options.keepFieldValues).each(function() {
var elem = $(this);
// If this is a checkbox or radiobutton, uncheck it.
// This fixes Issue 1, reported by Wilson.Andrew.J:
if (elem.is('input:checkbox') || elem.is('input:radio')) {
elem.attr('checked', false);
} else {
elem.val('');
}
});
}
// FIXME: Perhaps using $.data would be a better idea?
options.formTemplate = template;
if ($$.is('TR')) {
// If forms are laid out as table rows, insert the
// "add" button in a new table row:
var numCols = $$.eq(0).children().length, // This is a bit of an assumption :|
buttonRow = $('<tr><td colspan="' + numCols + '"><a class="' + options.addCssClass + '" href="javascript:void(0)">' + options.addText + '</a></tr>')
.addClass(options.formCssClass + '-add');
$$.parent().append(buttonRow);
if (hideAddButton) buttonRow.hide();
addButton = buttonRow.find('a');
} else {
// Otherwise, insert it immediately after the last form:
$$.filter(':last').after('<a class="' + options.addCssClass + '" href="javascript:void(0)">' + options.addText + '</a>');
addButton = $$.filter(':last').next();
if (hideAddButton) addButton.hide();
}
addButton.click(function() {
var formCount = parseInt(totalForms.val()),
row = options.formTemplate.clone(true).removeClass('formset-custom-template'),
buttonRow = $($(this).parents('tr.' + options.formCssClass + '-add').get(0) || this),
delCssSelector = $.trim(options.deleteCssClass).replace(/\s+/g, '.');
applyExtraClasses(row, formCount);
row.insertBefore(buttonRow).show();
row.find(childElementSelector).each(function() {
updateElementIndex($(this), options.prefix, formCount);
});
totalForms.val(formCount + 1);
// Check if we're above the minimum allowed number of forms -> show all delete link(s)
if (showDeleteLinks()){
$('a.' + delCssSelector).each(function(){$(this).show();});
}
// Check if we've exceeded the maximum allowed number of forms:
if (!showAddButton()) buttonRow.hide();
// If a post-add callback was supplied, call it with the added form:
if (options.added) options.added(row);
return false;
});
}
return $$;
};
/* Setup plugin defaults */
$.fn.formset.defaults = {
prefix: 'form', // The form prefix for your django formset
formTemplate: null, // The jQuery selection cloned to generate new form instances
addText: 'add another', // Text for the add link
deleteText: 'remove', // Text for the delete link
addCssClass: 'add-row', // CSS class applied to the add link
deleteCssClass: 'delete-row', // CSS class applied to the delete link
formCssClass: 'dynamic-form', // CSS class applied to each form in a formset
extraClasses: [], // Additional CSS classes, which will be applied to each form in turn
keepFieldValues: '', // jQuery selector for fields whose values should be kept when the form is cloned
added: null, // Function called each time a new form is added
removed: null // Function called each time a form is deleted
};
})(jQuery);

View File

@@ -4,10 +4,12 @@
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//kasu.at/piwik/";
var u="/piwik/";
_paq.push(['setTrackerUrl', u+'piwik.php']);
_paq.push(['setSiteId', '1']);
_paq.push(['disableCookies']);
_paq.push(['trackPageView']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
})();
/* End Piwik Code */
/* End Piwik Code */

View File

@@ -163,7 +163,7 @@ ul {
list-style: circle outside;
padding-left: 2em;
margin-top:0.5em;
li {margin-bottom: 0.5em}
}
ul.info {
@@ -258,6 +258,7 @@ ul.tabs {
padding: 0;
}
.error, ul.errorlist li {color: #a40000;}
input.error {border-color:#a40000; background-color: rgba(164, 0, 0, 0.25);}
.game h2 {margin: 0.5em 0;}
.grid_1,.grid_2,.grid_3,.grid_4,.grid_5,.grid_6,.grid_7,.grid_8,.grid_9,.grid_10,.grid_11,.grid_12
{

View File

@@ -71,7 +71,7 @@
<ul class="fa-ul">
<li><span class="fa-li fa fa-clock-o"></span><strong>{% trans "Since" %}:</strong> {{current_event.start|timesince}}</li>
<li><span class="fa-li fa fa-calendar"></span><strong>{% trans "Start" %}:</strong>
<time datetime="{{current_event.start|date:'c'}}">{{current_event.start|date:'DATETIME_FORMAT'}}</time>
<time datetime="{{current_event.start|date:'Y-m-d\TH:i:sO'}}">{{current_event.start|date:'DATETIME_FORMAT'}}</time>
</li>
<li><span class="fa-li fa fa-map-marker"></span><strong>{% trans "Location" %}:</strong> {{ current_event.location }}
- {{current_event.location.street_address}}, {{current_event.location.postal_code}} {{current_event.location.locality}}
@@ -79,13 +79,13 @@
</ul>
<div class="right"><a class="button" href="{{current_event.get_absolute_url}}">
{% trans "More Details" %} <span class="fa fa-arrow-right"></span></a></div>
{% else %}
{% elif next_event %}
<h2>{% trans "Next Event" %}</h2>
<h3>{{ next_event.name}}</h3>
<ul class="fa-ul">
<li><span class="fa-li fa fa-clock-o"></span><strong>{% trans "in" %}:</strong> {{next_event.start|timeuntil}}</li>
<li><span class="fa-li fa fa-calendar"></span><strong>{% trans "Start" %}:</strong>
<time datetime="{{next_event.start|date:'c'}}">{{next_event.start|date:'DATETIME_FORMAT' }}</time>
<time datetime="{{next_event.start|date:'Y-m-d\TH:i:sO'}}">{{next_event.start|date:'DATETIME_FORMAT' }}</time>
</li>
<li><span class="fa-li fa fa-map-marker"></span><strong>{% trans "Location" %}:</strong> {{ next_event.location }}
- {{next_event.location.street_address}}, {{next_event.location.postal_code}} {{next_event.location.locality}}
@@ -101,8 +101,10 @@
{% for event in upcoming_events %}
<li><span class="fa-li fa fa-calendar-o"></span>
<a href="{{ event.get_absolute_url}}">
<time datetime="{{event.start|date:'c'}}">{{event.start|date:'D d. M:'}}</time>
<time datetime="{{event.start|date:'Y-m-d\TH:i:sO'}}">{{event.start|date:'D d. M:'}}</time>
{{event.name}}</a></li>
{% empty %}
<li>{% trans 'No events found' %}.</li>
{% endfor %}
</ul>
{% endblock %}
@@ -185,12 +187,6 @@
{% trans "no user logged in" %} -
<a rel="nofollow" href="{% url 'membership-register' %}">{% trans "register" %}</a>
<a rel="nofollow" href="{% url 'login' %}?next={{ request.path_info }}">{% trans "login" %}</a>
<a rel="nofollow" href="{% url 'social:begin' 'facebook' %}" class="fa fa-facebook"
title="{% trans 'Login with Facebook' %}" aria-label="{% trans 'Login with Facebook' %}"></a>
<a rel="nofollow" href="{% url 'social:begin' 'twitter' %}" class="fa fa-twitter"
title="{% trans 'Login with Twitter' %}" aria-label="{% trans 'Login with Twitter' %}"></a>
<a rel="nofollow" href="{% url 'social:begin' 'google-oauth2' %}" class="fa fa-google"
title="{% trans 'Login with Google' %}" aria-label="{% trans 'Login with Google' %}"></a>
{% endif %}
</nav>
</body>

View File

@@ -7,7 +7,7 @@
</div>
<header class="comment_header">
<h3><a href="{{ comment.user.get_profile.get_absolute_url }}" class="user">{{comment.user}}</a></h3>
<time datetime="{{comment.submit_date|date:'Y-m-d'}}" class="submit_date">{{comment.submit_date|timesince}}</time>
<time datetime="{{comment.submit_date|date:'Y-m-d\TH:i:sO'}}" class="submit_date">{{comment.submit_date|timesince}}</time>
</header>
<div class="comment_text">{{comment.comment}}</div>
</article>

View File

@@ -0,0 +1,4 @@
{% with type="date" %}
{% include "django/forms/widgets/html5input.html" %}
{% endwith %}

View File

@@ -0,0 +1,4 @@
{% with type="datetime-local" %}
{% include "django/forms/widgets/html5input.html" %}
{% endwith %}

View File

@@ -0,0 +1 @@
<input type="{{ type|default:'text' }}" name="{{ widget.name }}"{% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}{% include "django/forms/widgets/attrs.html" %} />

View File

@@ -0,0 +1,4 @@
{% with type="time" %}
{% include "django/forms/widgets/html5input.html" %}
{% endwith %}

View File

@@ -5,8 +5,8 @@
<label {% if field.html_name != 'recaptcha' %} for="id_{{ field.html_name}}" {% endif %}
class="field_name {{ field.css_classes }}">{{ field.label}}</label>
{{ field }}
{% if field.help_text and not field.field.widget.input_type %}
{{field.help_text}}
{% if field.field.widget.input_type == 'checkbox' %}
<label for="id_{{field.name}}">{{field.help_text|safe}}</label>
{% elif field.help_text %}
<p class="help_text">{{field.help_text}}</p>
{% endif %}

View File

@@ -17,7 +17,7 @@
<h2><a href="{{article.get_absolute_url}}">{{article.headline}}</a></h2>
<ul class="info">
<li><span class="fa fa-calendar"></span>
<time datetime="{{article.date_created|date:'Y-m-d H:i'}}">{{ article.date_created|date:'DATE_FORMAT' }}</time>
<time datetime="{{article.date_created|date:'Y-m-d\TH:i:sO'}}">{{ article.date_created|date:'DATE_FORMAT' }}</time>
</li>
<li><span class="fa fa-user"></span> {{ article.author }}
</li>
@@ -48,7 +48,7 @@
From <a href="{{user_link}}">{{author}}</a> in
<a href="{{comment_link}}">&ldquo;{{object}}&rdquo;</a>
since
<time datetime="{{submit_date}}">{{since}}</time>
<time datetime="{{submit_date|date:'Y-m-d\TH:i:sO'}}">{{since}}</time>
{% endblocktrans %}
</li>
{% endfor %}

View File

@@ -29,13 +29,13 @@ sitemaps = {
urlpatterns = [ # Ignore PyLintBear (C0103)
url(r'^$', views.StartPage.as_view()),
url(r'^404/$', TemplateView.as_view(template_name='404.html')),
url(r'^add_page/(?P<path>[\+\.\-\d\w\/]+)/$',
url(r'^add_page/(?P<path>[\+\.\-\d\w\/]*)$',
views.PageAddForm.as_view(), name='add-page'),
url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
url(r'^admin/', include(admin.site.urls)),
url(r'^admin/', admin.site.urls),
url(r'^ckeditor/', include('ckeditor_uploader.urls')),
url(r'^comments/', include('django_comments.urls')),
url(r'^edit_page/(?P<path>[\+\.\-\d\w\/]+)/$',
url(r'^edit_page/(?P<path>[\+\.\-\d\w\/]*)$',
views.PageEditForm.as_view(), name='edit-page'),
url(r'^events/', include('events.urls')),
url(r'^events.ics$', EventListIcal.as_view(), name='events-ical'),
@@ -62,7 +62,6 @@ urlpatterns = [ # Ignore PyLintBear (C0103)
url(r'^(?P<path>[\-\d\w\/]+)\.html$',
views.PageHtml.as_view(), name='view-page'),
url(r'^(?P<path>[\-\d\w\/]+)\.pdf$', views.PagePdf.as_view()),
url('', include('social_django.urls', namespace='social'))
]
if settings.DEBUG:

View File

@@ -14,8 +14,8 @@ if VENV_PATH not in sys.path:
if SOURCE_PATH not in sys.path:
sys.path.append(SOURCE_PATH)
from django.core.wsgi import get_wsgi_application # Ignore PyLintBear (C0413) # Ignore PyLintBear (C0413)
from django.core.wsgi import get_wsgi_application
os.environ['DJANGO_SETTINGS_MODULE'] = 'kasu.settings'
application = get_wsgi_application() # Ignore PyLintBear (C0103) # Ignore PyLintBear (C0103)
application = get_wsgi_application()

149
src/kasu/xlsx.py Normal file
View File

@@ -0,0 +1,149 @@
"""
Helper to generate XLSX Spreadsheets in an uniform way.
"""
from datetime import date
import openpyxl
from openpyxl.styles import NamedStyle, Font, Border, Side
DEFAULT_FONT = Font(name='Philosopher', size=10, bold=False, color='000000')
THIN_BORDER = Border(
bottom=Side(style='thin', color="d3d7cf"),
top=Side(style='thin', color="d3d7cf"))
XLSX_STYLES = dict()
XLSX_STYLES['Content'] = NamedStyle(
name='Content',
font=DEFAULT_FONT,
border=THIN_BORDER
)
XLSX_STYLES['Headline'] = NamedStyle(
name="Headline",
font=openpyxl.styles.Font(name='Philosopher', size=11,
bold=True, color='ffffff'),
fill=openpyxl.styles.PatternFill(fill_type='solid',
start_color='a40000',
end_color='a40000')
)
XLSX_STYLES['Date'] = NamedStyle(
name='Date',
font=DEFAULT_FONT,
border=THIN_BORDER,
number_format='dd.mm.yyyy'
)
XLSX_STYLES['Date Time'] = NamedStyle(
name='Date Time',
font=DEFAULT_FONT,
border=THIN_BORDER,
number_format='dd.mm.yyyy hh:MM'
)
XLSX_STYLES['Float'] = NamedStyle(
name='Float',
font=DEFAULT_FONT,
border=THIN_BORDER,
number_format='#,##0.00'
)
XLSX_STYLES['Integer'] = NamedStyle(
name='Integer',
font=DEFAULT_FONT,
border=THIN_BORDER,
number_format='#,##0'
)
def getattr_recursive(obj, attr_string):
"""A recusive version of gettattr. the attr_string is splitted on the ".".
:param obj: a python object.
:param attr_string: the desired attribute of the object.
:return: a getattr_recursice(obj, 'attr1.attr2') will return the value of attr2 of attr1 from obj
"""
attr_list = attr_string.split('.')
return_value = None
for attr in attr_list:
return_value = getattr(obj, attr)
obj = return_value
return return_value
class Workbook(object):
workbook = None
def __init__(self):
"""Generate an XLSX Workbook in memory
:rtype: object
"""
self.workbook = openpyxl.Workbook()
[self.workbook.add_named_style(style)
for style in XLSX_STYLES.values()]
[self.workbook.remove(sheet) for sheet in self.workbook.worksheets]
def generate_sheet(self, title, columns_settings, object_list,
orientation='landscape'):
"""
:param title: Title of the Sheet
:param columns_settings: a list of dicts for the settings of each column
:param object_list: List of objects that should be added to the sheet
:param orientation: Paper Orientation 'landscape' or 'portrait'
"""
row = 1
ws = self.workbook.create_sheet()
ws.title = title
ws.syncHorizontal = True
ws.filterMode = True
# setup print orientation
ws.page_setup.fitToHeight = 0
ws.page_setup.fitToWidth = 1
if orientation == 'landscape':
ws.page_setup.orientation = ws.ORIENTATION_LANDSCAPE
else:
ws.page_setup.orientation = ws.ORIENTATION_PORTRAIT
ws.page_setup.paperSize = ws.PAPERSIZE_A4
ws.print_options.horizontalCentered = True
# setup page header
ws.oddHeader.left.text = title
ws.oddHeader.left.size = 14
ws.oddHeader.left.font = "Amerika Sans"
ws.oddHeader.left.color = "000000"
ws.oddHeader.right.text = str(date.today())
ws.oddHeader.right.size = 14
ws.oddHeader.right.font = "Amerika Sans"
ws.oddHeader.right.color = "000000"
# write table header
for column, data in enumerate(columns_settings, 1):
cell = ws.cell(column=column, row=row, value=data['title'])
cell.style = 'Headline'
row += 1
# write the table content
for line in object_list:
for column, settings in enumerate(columns_settings, 1):
cell = ws.cell(column=column, row=row,
value=getattr_recursive(line, settings['attr']))
cell.style = settings['style']
row += 1
# write table footer
for column, settings in enumerate(columns_settings, 1):
cell = ws.cell(column=column, row=row,
value=settings.get('footer'))
cell.style = settings['style']
row += 1
# set column widths
for settings in columns_settings:
ws.column_dimensions[settings['col']].width = settings['width']
def save(self, *args, **kwargs):
return self.workbook.save(*args, **kwargs)

View File

@@ -5,10 +5,11 @@ Created on 04.10.2011
@author: christian
"""
from django.contrib.auth import get_user_model
from django import forms
from django.contrib.auth import get_user_model
from django.utils.translation import ugettext as _
from events.models import Event
from . import models
USER_MODEL = get_user_model()
@@ -47,6 +48,17 @@ class HanchanForm(forms.ModelForm):
self.fields[player_input_score].widget.attrs['type'] = 'number'
self.fields[player].queryset = player_queryset
def is_valid(self):
ret = forms.Form.is_valid(self)
for field, errors in self.errors.items():
message = ", ".join(set(errors))
print(type(field), type(errors))
self.fields[field].widget.attrs.update({
'class': self.fields[field].widget.attrs.get('class', '') + ' error',
'title': message
})
return ret
class HanchanAdminForm(HanchanForm):
""" Extends the HanchanForm for users with admin privileges.
@@ -58,3 +70,9 @@ class HanchanAdminForm(HanchanForm):
""" Extend the formfields to add the confirmed checkbox. """
model = models.Hanchan
fields = HanchanForm.Meta.fields + ('confirmed',)
HanchanFormset = forms.inlineformset_factory(Event, models.Hanchan,
form=HanchanForm,
extra=1,
can_delete=True)

View File

@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: kasu.mahjong_ranking\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-04-27 09:49+0200\n"
"PO-Revision-Date: 2018-04-27 09:54+0105\n"
"POT-Creation-Date: 2019-12-13 23:38+0100\n"
"PO-Revision-Date: 2018-05-08 00:20+0105\n"
"Last-Translator: b'Christian Berg <kasu@xendynastie.at>'\n"
"Language-Team: Kasu <verein@kasu.at>\n"
"Language: de\n"
@@ -17,21 +17,21 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 1.8.9\n"
"X-Translated-Using: django-rosetta 0.7.14\n"
"X-Translated-Using: django-rosetta 0.8.1\n"
#: admin.py:26
#: admin.py:24
msgid "Recalculate"
msgstr "Neuberechnen"
#: admin.py:36
#: admin.py:34
msgid "Confirm"
msgstr "Bestätigen"
#: admin.py:46
#: admin.py:44
msgid "Set unconfirmed"
msgstr "Als unbestätigt markieren"
#: forms.py:21
#: forms.py:22
msgid "start"
msgstr "Beginn"
@@ -57,8 +57,8 @@ msgstr "Spieler 1"
#: templates/mahjong_ranking/eventranking_list.html:21
#: templates/mahjong_ranking/hanchan_confirm_delete.html:16
#: templates/mahjong_ranking/hanchan_form.html:19
#: templates/mahjong_ranking/kyudanranking_list.html:35
#: templates/mahjong_ranking/seasonranking_list.html:32
#: templates/mahjong_ranking/kyudanranking_list.html:30
#: templates/mahjong_ranking/seasonranking_list.html:31
msgid "Score"
msgstr "Punkte"
@@ -89,7 +89,7 @@ msgstr "Wurde bestätigt"
msgid "Only valid and confirmed Hanchans will be counted in the rating."
msgstr "Nur gültige und bestätigte Hanchans kommen in die Wertung."
#: models.py:179 models.py:603 templates/mahjong_ranking/ladder_redbox.html:29
#: models.py:179 models.py:607 templates/mahjong_ranking/ladder_redbox.html:29
#: templates/mahjong_ranking/player_ladder_score.html:63
msgid "Season"
msgstr "Saison"
@@ -127,14 +127,57 @@ msgstr "Spielstand ist weniger als 100.000 Punkte"
msgid "Gamescore is over 100.000 Pt."
msgstr "Spielstand ist über 100.000 Punkte."
#: models.py:356
#: models.py:362
msgid "Kyū/Dan Ranking"
msgstr "Kyū/Dan Wertung"
#: models.py:357
#: models.py:363
msgid "Kyū/Dan Rankings"
msgstr "Kyū/Dan Wertungen"
#: templates/mahjong_ranking/eventhanchan_form.html:11
#: templates/mahjong_ranking/eventhanchan_list.html:55
msgid "Edit Hanchans"
msgstr "Hanchans bearbeiten"
#: templates/mahjong_ranking/eventhanchan_form.html:49
#: templates/mahjong_ranking/hanchan_form.html:58
msgid "Total"
msgstr "Total"
#: templates/mahjong_ranking/eventhanchan_form.html:51
msgid "Difference"
msgstr "Unterschied"
#: templates/mahjong_ranking/eventhanchan_form.html:103
#: templates/mahjong_ranking/eventhanchan_list.html:56
#: templates/mahjong_ranking/eventranking_list.html:52
#: templates/mahjong_ranking/hanchan_form.html:4
#: templates/mahjong_ranking/hanchan_form.html:14
msgid "Add Hanchan"
msgstr "Hanchan hinzufügen"
#: templates/mahjong_ranking/eventhanchan_form.html:105
#: templates/mahjong_ranking/eventhanchan_list.html:37
#: templates/mahjong_ranking/hanchan_confirm_delete.html:4
#: templates/mahjong_ranking/hanchan_confirm_delete.html:33
#: templates/mahjong_ranking/player_dan_score.html:44
#: templates/mahjong_ranking/player_invalid_score.html:33
#: templates/mahjong_ranking/player_kyu_score.html:41
#: templates/mahjong_ranking/player_ladder_score.html:52
msgid "Delete Hanchan"
msgstr "Hanchan löschen"
#: templates/mahjong_ranking/eventhanchan_form.html:118
#: templates/mahjong_ranking/hanchan_form.html:71
msgid "back"
msgstr "Zurück"
#: templates/mahjong_ranking/eventhanchan_form.html:119
#: templates/mahjong_ranking/hanchan_form.html:72
msgid "save"
msgstr "Speichern"
#: templates/mahjong_ranking/eventhanchan_list.html:7
msgid "Played Hanchans"
msgstr "Gespielte Hanchans"
@@ -157,16 +200,6 @@ msgstr "Dan Punkte"
msgid "Kyu Points"
msgstr "Kyu Punkte"
#: templates/mahjong_ranking/eventhanchan_list.html:37
#: templates/mahjong_ranking/hanchan_confirm_delete.html:4
#: templates/mahjong_ranking/hanchan_confirm_delete.html:33
#: templates/mahjong_ranking/player_dan_score.html:44
#: templates/mahjong_ranking/player_invalid_score.html:33
#: templates/mahjong_ranking/player_kyu_score.html:41
#: templates/mahjong_ranking/player_ladder_score.html:52
msgid "Delete Hanchan"
msgstr "Hanchan löschen"
#: templates/mahjong_ranking/eventhanchan_list.html:43
#: templates/mahjong_ranking/hanchan_form.html:4
#: templates/mahjong_ranking/hanchan_form.html:14
@@ -186,20 +219,13 @@ msgstr "Für diese Veranstaltung wurde noch keine Hanchan eingetragen."
msgid "Edit Event"
msgstr "Veranstaltung bearbeiten"
#: templates/mahjong_ranking/eventhanchan_list.html:55
#: templates/mahjong_ranking/eventranking_list.html:52
#: templates/mahjong_ranking/hanchan_form.html:4
#: templates/mahjong_ranking/hanchan_form.html:14
msgid "Add Hanchan"
msgstr "Hanchan hinzufügen"
#: templates/mahjong_ranking/eventranking_list.html:4
#: templates/mahjong_ranking/eventranking_list.html:5
msgid "Tournament Ranking"
msgstr "Turnierwertung"
#: templates/mahjong_ranking/eventranking_list.html:12
#: templates/mahjong_ranking/kyudanranking_list.html:30
#: templates/mahjong_ranking/kyudanranking_list.html:25
#: templates/mahjong_ranking/seasonranking_list.html:23
msgid "Rank"
msgstr "Rang"
@@ -217,12 +243,11 @@ msgid "Nickname"
msgstr "Spitzname"
#: templates/mahjong_ranking/eventranking_list.html:15
#: templates/mahjong_ranking/seasonranking_list.html:26
msgid "Name"
msgstr "Name"
#: templates/mahjong_ranking/eventranking_list.html:16
#: templates/mahjong_ranking/seasonranking_list.html:27
#: templates/mahjong_ranking/seasonranking_list.html:26
msgid "Average"
msgstr "Durchschnitt"
@@ -231,22 +256,22 @@ msgstr "Durchschnitt"
#: templates/mahjong_ranking/player_invalid_score.html:15
#: templates/mahjong_ranking/player_kyu_score.html:16
#: templates/mahjong_ranking/player_ladder_score.html:16
#: templates/mahjong_ranking/seasonranking_list.html:31
#: templates/mahjong_ranking/seasonranking_list.html:30
msgid "Placement"
msgstr "Platzierung"
#: templates/mahjong_ranking/eventranking_list.html:22
#: templates/mahjong_ranking/seasonranking_list.html:33
#: templates/mahjong_ranking/seasonranking_list.html:32
msgid "count"
msgstr "Anzahl"
#: templates/mahjong_ranking/eventranking_list.html:23
#: templates/mahjong_ranking/seasonranking_list.html:34
#: templates/mahjong_ranking/seasonranking_list.html:33
msgid "good"
msgstr "gut"
#: templates/mahjong_ranking/eventranking_list.html:24
#: templates/mahjong_ranking/seasonranking_list.html:35
#: templates/mahjong_ranking/seasonranking_list.html:34
msgid "won"
msgstr "gewonnen"
@@ -262,32 +287,12 @@ msgstr "Löschen"
msgid "Player"
msgstr "Spieler"
#: templates/mahjong_ranking/hanchan_form.html:58
msgid "Total"
msgstr "Total"
#: templates/mahjong_ranking/hanchan_form.html:71
msgid "back"
msgstr "Zurück"
#: templates/mahjong_ranking/hanchan_form.html:72
msgid "save"
msgstr "Speichern"
#: templates/mahjong_ranking/kyudanranking_list.html:4
#| msgid "Player List"
#: templates/mahjong_ranking/kyudanranking_list.html:9
msgid "Players list"
msgstr "Spielerliste"
#: templates/mahjong_ranking/kyudanranking_list.html:9
msgid "Player List"
msgstr "Spieler Liste"
#: templates/mahjong_ranking/kyudanranking_list.html:25
msgid "Full Name"
msgstr "Voller Name"
#: templates/mahjong_ranking/kyudanranking_list.html:40
#: templates/mahjong_ranking/kyudanranking_list.html:35
msgid "Games Total"
msgstr "Spiele total"
@@ -371,20 +376,23 @@ msgstr "Ende"
msgid "Participants"
msgstr "Teilnehmer"
#: views.py:102
#: views.py:116
#, python-format
msgid "%s has been updated successfully."
msgstr "%s wurde erfolgreich aktualisiert."
#: views.py:105
#: views.py:119
#, python-format
msgid "%s has been added successfully. You can now add a new one."
msgstr "%s wurde erfolgreich hinzugefügt. Du kannst eine neue eintragen."
#: views.py:169
#: views.py:219
msgid "No user found matching the name {}"
msgstr "Kein Benutzer mit dem Namen %s gefunden"
#~ msgid "Full Name"
#~ msgstr "Voller Name"
#~ msgid "Event does not exist"
#~ msgstr "Veranstaltung existiert nicht"

View File

@@ -1,214 +0,0 @@
"""Export Mahjong Rankings as excel files."""
import os
from datetime import date, time, datetime
import openpyxl
from django.conf import settings
from django.core.management.base import BaseCommand
from django.utils import timezone
from django.utils.dateparse import parse_date
from django.core.mail import EmailMessage
from mahjong_ranking.models import SeasonRanking, KyuDanRanking
THIN_BORDER = openpyxl.styles.Side(style='thin', color="d3d7cf")
HEADING_STYLE = openpyxl.styles.NamedStyle(name="heading")
HEADING_STYLE.font = openpyxl.styles.Font(name='Philosopher', size=11,
bold=True, color='ffffff')
HEADING_STYLE.fill = openpyxl.styles.PatternFill(fill_type='solid',
start_color='a40000',
end_color='a40000')
DEFAULT_STYLE = openpyxl.styles.NamedStyle(name='content')
DEFAULT_STYLE.font = openpyxl.styles.Font(name='Philosopher', size=10,
bold=False, color='000000')
DEFAULT_STYLE.border = openpyxl.styles.Border(bottom=THIN_BORDER,
top=THIN_BORDER)
INT_STYLE = openpyxl.styles.NamedStyle(name='int')
INT_STYLE.font = DEFAULT_STYLE.font
INT_STYLE.border = DEFAULT_STYLE.border
INT_STYLE.number_format = '#,##0'
FLOAT_STYLE = openpyxl.styles.NamedStyle(name='float')
FLOAT_STYLE.font = DEFAULT_STYLE.font
FLOAT_STYLE.border = DEFAULT_STYLE.border
FLOAT_STYLE.number_format = '#,##0.00'
DATE_STYLE = openpyxl.styles.NamedStyle(name='date')
DATE_STYLE.font = DEFAULT_STYLE.font
DATE_STYLE.border = DEFAULT_STYLE.border
DATE_STYLE.number_format = 'dd.mm.yyyy'
MAIL_BODY = """
Hallo! Ich bin's dein Server.
Ich habe gerade die Mahjong Rankings als Excel exportiert und dachte mir das
ich sie dir am besten gleich schicke.
Bitte versuche nicht auf diese E-Mail zu antworten.
Ich bin nur ein dummes Programm.
mit lieben Grüßen
Der Kasu Server
"""
def geneate_excel():
"""Generate an excel .xlsx spreadsheet from json data of the kyu/dan
rankings.
:param json_data: The ladder ranking as JSON export."""
workbook = openpyxl.Workbook()
workbook.add_named_style(HEADING_STYLE)
workbook.add_named_style(DEFAULT_STYLE)
workbook.add_named_style(INT_STYLE)
workbook.add_named_style(FLOAT_STYLE)
workbook.add_named_style(DATE_STYLE)
for sheet in workbook.worksheets:
workbook.remove(sheet)
return workbook
def generate_sheet(workbook, title, columns_settings, json_data):
row = 1
ws = workbook.create_sheet()
ws.title = title
ws.syncHorizontal = True
ws.filterMode = True
# setup print orientation
ws.page_setup.orientation = ws.ORIENTATION_PORTRAIT
ws.page_setup.paperSize = ws.PAPERSIZE_A4
ws.page_setup.fitToWidth = True
ws.print_options.horizontalCentered = True
# setup page header
ws.oddHeader.left.text = title
ws.oddHeader.left.size = 14
ws.oddHeader.left.font = "Amerika Sans"
ws.oddHeader.left.color = "000000"
ws.oddHeader.right.text = str(date.today())
ws.oddHeader.right.size = 14
ws.oddHeader.right.font = "Amerika Sans"
ws.oddHeader.right.color = "000000"
# write table header
for column, data in enumerate(columns_settings, 1):
cell = ws.cell(column=column, row=row, value=data['title'])
cell.style = 'heading'
# write the table content
for line in json_data:
row += 1
for column, settings in enumerate(columns_settings, 1):
cell = ws.cell(column=column, row=row, value=line[settings['attr']])
cell.style = settings['style']
# set column widths
for settings in columns_settings:
ws.column_dimensions[settings['col']].width = settings['width']
def export_season_rankings(workbook, until):
SeasonRanking.objects.update(until=until)
json_data = SeasonRanking.objects.json_data()
title = "Mahjong Ladder - {}".format(until.year)
columns_settings = (
{'col': 'A', 'title': 'Rang', 'attr': 'placement', 'style': 'int',
'width': 8},
{'col': 'B', 'title': 'Spitzname', 'attr': 'username',
'style': 'content',
'width': 25},
{'col': 'C', 'title': '⌀ Platz', 'attr': 'avg_placement',
'style': 'float', 'width': 8},
{'col': 'D', 'title': '⌀ Punkte', 'attr': 'avg_score',
'style': 'float', 'width': 12},
{'col': 'E', 'title': 'Hanchans', 'attr': 'hanchan_count',
'style': 'int', 'width': 10},
{'col': 'F', 'title': 'Gut', 'attr': 'good_hanchans',
'style': 'int', 'width': 5},
{'col': 'G', 'title': 'Gewonnen', 'attr': 'won_hanchans',
'style': 'int', 'width': 10},
)
generate_sheet(
workbook=workbook,
title=title,
columns_settings=columns_settings,
json_data=json_data)
def export_kyu_dan_rankings(workbook, until):
KyuDanRanking.objects.update(until=until)
json_data = KyuDanRanking.objects.json_data()
title = "Kyū & Dan Rankings"
columns_settings = (
{'col': 'A', 'title': 'Spitzname', 'attr': 'username',
'style': 'content', 'width': 14},
{'col': 'B', 'title': 'Voller Name', 'attr': 'full_name',
'style': 'content', 'width': 20},
{'col': 'C', 'title': 'Rang', 'attr': 'rank',
'style': 'content', 'width': 8},
{'col': 'D', 'title': 'Punkte', 'attr': 'points',
'style': 'int', 'width': 8},
{'col': 'E', 'title': 'Hanchans', 'attr': 'hanchan_count',
'style': 'int', 'width': 10},
{'col': 'F', 'title': 'Gut', 'attr': 'good_hanchans',
'style': 'int', 'width': 5},
{'col': 'G', 'title': 'Gewonnen', 'attr': 'won_hanchans',
'style': 'int', 'width': 8},
{'col': 'H', 'title': 'letzte Hanchan', 'attr': 'last_hanchan_date',
'style': 'date', 'width': 16},
)
generate_sheet(
workbook=workbook,
title=title,
columns_settings=columns_settings,
json_data=json_data)
class Command(BaseCommand):
"""Exports the SeasonRankings"""
filename = str()
until = datetime
def add_arguments(self, parser):
parser.add_argument(
'--until', nargs='?', type=parse_date,
default=date.today(), metavar='YYYY-MM-DD',
help='Calculate and export rankings until the given date.')
parser.add_argument(
'--mail', nargs='*', type=str, metavar='user@example.com',
help='Send the spreadsheet via eMail to the given recipient.')
def handle(self, *args, **options):
"""Exports the current ladder ranking in a spreadsheet.
This is useful as a backup in form of a hardcopy."""
self.until = timezone.make_aware(datetime.combine(
options['until'], time(23, 59, 59)
))
self.filename = os.path.join(
settings.RANKING_EXPORT_PATH,
'mahjong_rankings_{:%Y-%m-%d}.xlsx'.format(self.until)
)
workbook = geneate_excel()
export_season_rankings(workbook, until=self.until)
export_kyu_dan_rankings(workbook, until=self.until)
os.makedirs(settings.RANKING_EXPORT_PATH, exist_ok=True)
workbook.save(self.filename)
if options['mail']:
self.send_mail(options['mail'])
def send_mail(self, recipients):
mail = EmailMessage(
subject='Mahjong Rankings vom {:%d.%m.%Y}'.format(self.until),
body=MAIL_BODY,
from_email=settings.DEFAULT_FROM_EMAIL,
to=recipients)
mail.attach_file(self.filename)
mail.send()

View File

@@ -0,0 +1,126 @@
"""Export Mahjong Rankings as excel files."""
import os
from datetime import date, time, datetime
from django.conf import settings
from django.core.mail import EmailMessage
from django.core.management.base import BaseCommand
from django.utils import timezone
from django.utils.dateparse import parse_date
from kasu import xlsx
from mahjong_ranking.models import SeasonRanking, KyuDanRanking
MAIL_BODY = """
Hallo! Ich bin's dein Server.
Ich habe gerade die Mahjong Rankings als Excel exportiert und dachte mir das
ich sie dir am besten gleich schicke.
Bitte versuche nicht auf diese E-Mail zu antworten.
Ich bin nur ein dummes Programm.
mit lieben Grüßen
Der Kasu Server
"""
def export_season_rankings(workbook, until):
SeasonRanking.objects.update(until=until)
season = until.year if until else date.today().year
object_list = SeasonRanking.objects.season_rankings()
title = "Mahjong Ladder - {}".format(season)
columns_settings = (
{'col': 'A', 'title': 'Rang', 'attr': 'placement', 'style': 'Integer',
'width': 8},
{'col': 'B', 'title': 'Spitzname', 'attr': 'user.username',
'style': 'Content',
'width': 25},
{'col': 'C', 'title': '⌀ Platz', 'attr': 'avg_placement',
'style': 'Float', 'width': 8},
{'col': 'D', 'title': '⌀ Punkte', 'attr': 'avg_score',
'style': 'Float', 'width': 12},
{'col': 'E', 'title': 'Hanchans', 'attr': 'hanchan_count',
'style': 'Integer', 'width': 10},
{'col': 'F', 'title': 'Gut', 'attr': 'good_hanchans',
'style': 'Integer', 'width': 5},
{'col': 'G', 'title': 'Gewonnen', 'attr': 'won_hanchans',
'style': 'Integer', 'width': 10},
)
workbook.generate_sheet(
title=title,
columns_settings=columns_settings,
object_list=object_list)
def export_kyu_dan_rankings(workbook, until):
KyuDanRanking.objects.update(until=until, force_recalc=True)
object_list = KyuDanRanking.objects.all()
title = "Kyū & Dan Rankings"
columns_settings = (
{'col': 'A', 'title': 'Spitzname', 'attr': 'user.username',
'style': 'Content', 'width': 14},
{'col': 'B', 'title': 'Voller Name', 'attr': 'user.full_name',
'style': 'Content', 'width': 20},
{'col': 'C', 'title': 'Rang', 'attr': 'rank',
'style': 'Content', 'width': 8},
{'col': 'D', 'title': 'Punkte', 'attr': 'points',
'style': 'Integer', 'width': 8},
{'col': 'E', 'title': 'Hanchans', 'attr': 'hanchan_count',
'style': 'Integer', 'width': 10},
{'col': 'F', 'title': 'Gut', 'attr': 'good_hanchans',
'style': 'Integer', 'width': 5},
{'col': 'G', 'title': 'Gewonnen', 'attr': 'won_hanchans',
'style': 'Integer', 'width': 10},
{'col': 'H', 'title': 'letzte Hanchan', 'attr': 'last_hanchan_date',
'style': 'Date', 'width': 16},
)
workbook.generate_sheet(
title=title,
columns_settings=columns_settings,
object_list=object_list)
class Command(BaseCommand):
"""Exports the SeasonRankings"""
filename = str()
until = datetime
def add_arguments(self, parser):
parser.add_argument(
'--until', nargs='?', type=parse_date,
default=date.today(), metavar='YYYY-MM-DD',
help='Calculate and export rankings until the given date.')
parser.add_argument(
'--mail', nargs='*', type=str, metavar='user@example.com',
help='Send the spreadsheet via eMail to the given recipient.')
def handle(self, *args, **options):
"""Exports the current ladder ranking in a spreadsheet.
This is useful as a backup in form of a hardcopy."""
self.until = timezone.make_aware(datetime.combine(
options['until'], time(23, 59, 59)
))
self.filename = os.path.join(
settings.RANKING_EXPORT_PATH,
'mahjong_rankings_{:%Y-%m-%d}.xlsx'.format(self.until)
)
workbook = xlsx.Workbook()
export_season_rankings(workbook, until=self.until)
export_kyu_dan_rankings(workbook, until=self.until)
os.makedirs(settings.RANKING_EXPORT_PATH, exist_ok=True)
workbook.save(self.filename)
if options['mail']:
self.send_mail(options['mail'])
def send_mail(self, recipients):
mail = EmailMessage(
subject='Mahjong Rankings vom {:%d.%m.%Y}'.format(self.until),
body=MAIL_BODY,
from_email=settings.DEFAULT_FROM_EMAIL,
to=recipients)
mail.attach_file(self.filename)
mail.send()

View File

@@ -3,11 +3,13 @@ Rest all dan points to 0 at a given date.
"""
from django.core.management.base import BaseCommand
from datetime import date, datetime, time
from datetime import datetime
from datetime import time
from mahjong_ranking import models
from django.utils.dateparse import parse_date
from django.utils import timezone
class Command(BaseCommand):
""" Recalculate all Kyu/Dan Rankings """
@@ -17,16 +19,23 @@ class Command(BaseCommand):
parser.add_argument('reset_date', type=parse_date)
def handle(self, *args, **options):
reset_date = timezone.make_aware(datetime.combine(options.get('reset_date'), time(23, 59, 59)))
# models.KyuDanRanking.objects.update(until=reset_date, force_recalc=True)
dan_rankigns = models.KyuDanRanking.objects.filter(dan__isnull=False)
for ranking in dan_rankigns:
legacy_attrs = [key for key in models.KyuDanRanking.__dict__.keys()
if key.startswith('legacy')]
legacy_attrs.remove('legacy_date')
reset_date = timezone.make_aware(datetime.combine(
options.get('reset_date'), time(23, 59, 59)))
models.KyuDanRanking.objects.update(
until=reset_date, force_recalc=True)
for ranking in models.KyuDanRanking.objects.filter(dan__gt=0):
print(ranking)
ranking.dan = 1
ranking.dan_points = 0
ranking.kyu = None
ranking.kyu_points = 0
ranking.wins_in_a_row = 0
ranking.legacy_date = reset_date.date()
ranking.legacy_hanchan_count = ranking.hanchan_count
ranking.legacy_dan_points = ranking.dan_points
ranking.legacy_kyu_points = ranking.kyu_points
for legacy_attr in legacy_attrs:
attr = legacy_attr.split("_", maxsplit=1)[1]
print(ranking, legacy_attr, attr, getattr(ranking, attr))
setattr(ranking, legacy_attr, getattr(ranking, attr))
ranking.save()

View File

@@ -4,30 +4,39 @@
Recalculate Mahjong Rankings...
"""
from django.core.management.base import BaseCommand
from datetime import date, datetime, time
from mahjong_ranking import models
from django.utils.dateparse import parse_date
from django.core.management.base import BaseCommand
from django.utils import timezone
from django.utils.dateparse import parse_date
from mahjong_ranking import models
class Command(BaseCommand):
""" Recalculate all Kyu/Dan Rankings """
help = "Recalculate all Kyu/Dan Rankings"
help = "Recalculate the Kyu/Dan Rankings"
def add_arguments(self, parser):
parser.add_argument('--since', nargs='?', type=parse_date)
parser.add_argument('--until', nargs='?', type=parse_date)
parser.add_argument('--forcerecalc', action='store_true')
parser.add_argument('-s', '--since', nargs='?', type=parse_date,
metavar='YYYY-MM-DD',
help='Use all Hanchans since the given date.')
parser.add_argument('-u', '--until', nargs='?', type=parse_date,
metavar='YYYY-MM-DD',
help='Only use Hanchans until the given date.')
parser.add_argument('-f', '--force', action='store_true',
help="Force the recalculation of all Hanchans.")
def handle(self, *args, **options):
since = options.get('since', None)
until = options.get('until', None)
force_recalc = options.get('forecerecalc', False)
force_recalc = options.get('force')
if isinstance(since, date):
since = datetime.combine(since, time(0, 0, 0))
since = timezone.make_aware(since)
if isinstance(until, date):
until = datetime.combine(until, time(23, 59, 59))
until = timezone.make_aware(until)
models.KyuDanRanking.objects.update(since=since, until=until, force_recalc=force_recalc)
models.KyuDanRanking.objects.update(since=since, until=until,
force_recalc=force_recalc)

View File

@@ -8,14 +8,14 @@ from django.conf import settings
class HanchanManager(models.Manager):
"""
The ObjectManager for models.Hanchan QuerySets.
It adds many specific filters that makes many queries much easier.
"""
use_for_related_fields = True
def confirmed(self, user=None, since=None, until=None, **filter_args):
""" Return all valid and confirmed Hanchans.
:param user: Only return Hanchans where this user participated.
:param since: only return Hanchans played since the given datetime
:param until: only return Hanchans played until the given datetime
@@ -23,8 +23,8 @@ class HanchanManager(models.Manager):
:return: QuerySet Object
"""
if user:
return self.user_hanchans(
user, confirmed=True, since=since, until=until, **filter_args)
return self.user_hanchans(user, confirmed=True, until=until,
**filter_args)
hanchans = self.filter(confirmed=True, **filter_args)
if since:
hanchans = hanchans.filter(start__gt=since)
@@ -32,7 +32,7 @@ class HanchanManager(models.Manager):
hanchans = hanchans.filter(start__lte=until)
return hanchans
def dan_hanchans(self, user, since = None, **filter_args):
def dan_hanchans(self, user, since=None, **filter_args):
""" Return all Hanchans where a specific user has participated and had
gain dan points and make his gamestats availabale.
@@ -53,7 +53,7 @@ class HanchanManager(models.Manager):
[hanchan.get_playerdata(user) for hanchan in queryset]
return queryset
def kyu_hanchans(self, user, since = None, **filter_args):
def kyu_hanchans(self, user, since=None, **filter_args):
""" Return all Hanchans where a specific user has participated and had
gain kyū points and make his gamestats availabale.
@@ -101,7 +101,7 @@ class HanchanManager(models.Manager):
)
queryset = queryset.filter(**filter_args)
if since:
queryset = queryset.filter(start__gt=since)
queryset = queryset.filter(start__gte=since)
if until:
queryset = queryset.filter(start__lte=until)
queryset = queryset.select_related().order_by('-start')

View File

@@ -1,12 +1,27 @@
"""Middleware to defer slow denormalization at the end of a request."""
from django.core.cache import cache
from mahjong_ranking import models
from . import LOGGER
from . import LOGGER
class DenormalizationUpdateMiddleware(object): # Ignore PyLintBear (R0903)
class DenormalizationUpdateMiddleware:
"""To recalculate everything in the queues at the end of a POST request."""
def __init__(self, get_response=None):
self.get_response = get_response
super().__init__()
def __call__(self, request):
response = None
if hasattr(self, 'process_request'):
response = self.process_request(request)
response = response or self.get_response(request)
if hasattr(self, 'process_response'):
response = self.process_response(request, response)
return response
def process_response(self, request, response): # Ignore PyLintBear (R0201)
"""Check and process the recalculation queues on each POST request.
@@ -16,7 +31,6 @@ class DenormalizationUpdateMiddleware(object): # Ignore PyLintBear (R0903)
"""
event_queue = set()
season_queue = set()
if request.method != 'POST':
return response

View File

@@ -1,12 +1,11 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('events', '0005_auto_20150907_2021'),
@@ -17,15 +16,19 @@ class Migration(migrations.Migration):
name='EventRanking',
fields=[
('id', models.AutoField(verbose_name='ID',
serialize=False, auto_created=True, primary_key=True)),
('placement', models.PositiveIntegerField(null=True, blank=True)),
serialize=False, auto_created=True,
primary_key=True)),
('placement',
models.PositiveIntegerField(null=True, blank=True)),
('avg_placement', models.FloatField(default=4)),
('avg_score', models.FloatField(default=0)),
('hanchan_count', models.PositiveIntegerField(default=0)),
('good_hanchans', models.PositiveIntegerField(default=0)),
('won_hanchans', models.PositiveIntegerField(default=0)),
('event', models.ForeignKey(to='events.Event')),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
('event', models.ForeignKey(to='events.Event',
on_delete=models.CASCADE)),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)),
],
options={
'ordering': ('placement', 'avg_placement', '-avg_score'),
@@ -35,10 +38,13 @@ class Migration(migrations.Migration):
name='Hanchan',
fields=[
('id', models.AutoField(verbose_name='ID',
serialize=False, auto_created=True, primary_key=True)),
serialize=False, auto_created=True,
primary_key=True)),
('start', models.DateTimeField(
help_text='Wichtig damit die richtigen Hanchans in die Wertung kommen.', verbose_name='Beginn')),
('player1_input_score', models.IntegerField(verbose_name='Punkte')),
help_text='Wichtig damit die richtigen Hanchans in die Wertung kommen.',
verbose_name='Beginn')),
('player1_input_score',
models.IntegerField(verbose_name='Punkte')),
('player1_game_score', models.PositiveIntegerField(
default=0, verbose_name='Punkte', editable=False)),
('player1_placement', models.PositiveSmallIntegerField(
@@ -50,8 +56,11 @@ class Migration(migrations.Migration):
('player1_bonus_points', models.SmallIntegerField(
null=True, editable=False, blank=True)),
('player1_comment', models.CharField(verbose_name='Anmerkung',
max_length=255, editable=False, blank=True)),
('player2_input_score', models.IntegerField(verbose_name='Punkte')),
max_length=255,
editable=False,
blank=True)),
('player2_input_score',
models.IntegerField(verbose_name='Punkte')),
('player2_game_score', models.PositiveIntegerField(
default=0, verbose_name='Punkte', editable=False)),
('player2_placement', models.PositiveSmallIntegerField(
@@ -63,8 +72,11 @@ class Migration(migrations.Migration):
('player2_bonus_points', models.SmallIntegerField(
null=True, editable=False, blank=True)),
('player2_comment', models.CharField(verbose_name='Anmerkung',
max_length=255, editable=False, blank=True)),
('player3_input_score', models.IntegerField(verbose_name='Punkte')),
max_length=255,
editable=False,
blank=True)),
('player3_input_score',
models.IntegerField(verbose_name='Punkte')),
('player3_game_score', models.PositiveIntegerField(
default=0, verbose_name='Punkte', editable=False)),
('player3_placement', models.PositiveSmallIntegerField(
@@ -76,8 +88,11 @@ class Migration(migrations.Migration):
('player3_bonus_points', models.SmallIntegerField(
null=True, editable=False, blank=True)),
('player3_comment', models.CharField(verbose_name='Anmerkung',
max_length=255, editable=False, blank=True)),
('player4_input_score', models.IntegerField(verbose_name='Punkte')),
max_length=255,
editable=False,
blank=True)),
('player4_input_score',
models.IntegerField(verbose_name='Punkte')),
('player4_game_score', models.PositiveIntegerField(
default=0, verbose_name='Punkte', editable=False)),
('player4_placement', models.PositiveSmallIntegerField(
@@ -89,22 +104,37 @@ class Migration(migrations.Migration):
('player4_bonus_points', models.SmallIntegerField(
null=True, editable=False, blank=True)),
('player4_comment', models.CharField(verbose_name='Anmerkung',
max_length=255, editable=False, blank=True)),
('comment', models.TextField(verbose_name='Anmerkung', blank=True)),
max_length=255,
editable=False,
blank=True)),
('comment',
models.TextField(verbose_name='Anmerkung', blank=True)),
('confirmed', models.BooleanField(
default=True, help_text='Nur g\xfcltige und best\xe4tigte Hanchans kommen in die Wertung.', verbose_name='Wurde best\xe4tigt')),
('player_names', models.CharField(max_length=255, editable=False)),
default=True,
help_text='Nur g\xfcltige und best\xe4tigte Hanchans kommen in die Wertung.',
verbose_name='Wurde best\xe4tigt')),
('player_names',
models.CharField(max_length=255, editable=False)),
('season', models.PositiveSmallIntegerField(
verbose_name='Saison', editable=False, db_index=True)),
('event', models.ForeignKey(to='events.Event')),
('event', models.ForeignKey(to='events.Event',
on_delete=models.CASCADE)),
('player1', models.ForeignKey(related_name='user_hanchan+',
verbose_name='Spieler 1', to=settings.AUTH_USER_MODEL)),
verbose_name='Spieler 1',
to=settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)),
('player2', models.ForeignKey(related_name='user_hanchan+',
verbose_name='Spieler 2', to=settings.AUTH_USER_MODEL)),
verbose_name='Spieler 2',
to=settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)),
('player3', models.ForeignKey(related_name='user_hanchan+',
verbose_name='Spieler 3', to=settings.AUTH_USER_MODEL)),
verbose_name='Spieler 3',
to=settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)),
('player4', models.ForeignKey(related_name='user_hanchan+',
verbose_name='Spieler 4', to=settings.AUTH_USER_MODEL)),
verbose_name='Spieler 4',
to=settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)),
],
options={
'ordering': ('-start',),
@@ -116,8 +146,11 @@ class Migration(migrations.Migration):
name='KyuDanRanking',
fields=[
('id', models.AutoField(verbose_name='ID',
serialize=False, auto_created=True, primary_key=True)),
('dan', models.PositiveSmallIntegerField(null=True, blank=True)),
serialize=False, auto_created=True,
primary_key=True)),
(
'dan',
models.PositiveSmallIntegerField(null=True, blank=True)),
('dan_points', models.PositiveIntegerField(default=0)),
('kyu', models.PositiveSmallIntegerField(
default=10, null=True, blank=True)),
@@ -128,7 +161,8 @@ class Migration(migrations.Migration):
('legacy_date', models.DateField(null=True, blank=True)),
('legacy_dan_points', models.PositiveIntegerField(default=0)),
('legacy_kyu_points', models.PositiveIntegerField(default=0)),
('user', models.OneToOneField(to=settings.AUTH_USER_MODEL)),
('user', models.OneToOneField(to=settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)),
],
options={
'ordering': ('-dan', '-dan_points', '-kyu_points'),
@@ -140,15 +174,19 @@ class Migration(migrations.Migration):
name='SeasonRanking',
fields=[
('id', models.AutoField(verbose_name='ID',
serialize=False, auto_created=True, primary_key=True)),
('season', models.PositiveSmallIntegerField(verbose_name='Saison')),
('placement', models.PositiveIntegerField(null=True, blank=True)),
serialize=False, auto_created=True,
primary_key=True)),
('season',
models.PositiveSmallIntegerField(verbose_name='Saison')),
('placement',
models.PositiveIntegerField(null=True, blank=True)),
('avg_placement', models.FloatField(null=True, blank=True)),
('avg_score', models.FloatField(null=True, blank=True)),
('hanchan_count', models.PositiveIntegerField(default=0)),
('good_hanchans', models.PositiveIntegerField(default=0)),
('won_hanchans', models.PositiveIntegerField(default=0)),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)),
],
options={
'ordering': ('placement', 'avg_placement', '-avg_score'),

View File

@@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.8 on 2017-12-14 12:18
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('mahjong_ranking', '0005_auto_20171115_0653'),
]
operations = [
migrations.AddField(
model_name='kyudanranking',
name='legacy_dan',
field=models.PositiveSmallIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='kyudanranking',
name='legacy_good_hanchans',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='kyudanranking',
name='legacy_kyu',
field=models.PositiveSmallIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='kyudanranking',
name='legacy_won_hanchans',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='kyudanranking',
name='max_dan_points',
field=models.PositiveIntegerField(default=0),
),
migrations.AlterField(
model_name='eventranking',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='hanchan',
name='player1',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='user_hanchan+', to=settings.AUTH_USER_MODEL, verbose_name='Spieler 1'),
),
migrations.AlterField(
model_name='hanchan',
name='player2',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='user_hanchan+', to=settings.AUTH_USER_MODEL, verbose_name='Spieler 2'),
),
migrations.AlterField(
model_name='hanchan',
name='player3',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='user_hanchan+', to=settings.AUTH_USER_MODEL, verbose_name='Spieler 3'),
),
migrations.AlterField(
model_name='hanchan',
name='player4',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='user_hanchan+', to=settings.AUTH_USER_MODEL, verbose_name='Spieler 4'),
),
migrations.AlterField(
model_name='kyudanranking',
name='legacy_dan_points',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='kyudanranking',
name='legacy_hanchan_count',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='kyudanranking',
name='legacy_kyu_points',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='seasonranking',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -9,8 +9,8 @@ from datetime import datetime, time
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
from django.db import models
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import ugettext as _
@@ -29,8 +29,9 @@ class EventRanking(models.Model):
Sie beschränken sich aber auf einen Event und werden nur dann angestossen,
wenn der Event als Turnier markiert wurde.
"""
user = models.ForeignKey(settings.AUTH_USER_MODEL)
event = models.ForeignKey(Event)
user = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.PROTECT)
event = models.ForeignKey(Event, on_delete=models.CASCADE)
placement = models.PositiveIntegerField(blank=True, null=True)
avg_placement = models.FloatField(default=4)
avg_score = models.FloatField(default=0)
@@ -86,7 +87,7 @@ class Hanchan(models.Model):
Es werden aber noch andere Tests durchgeführt, ob sie gültig ist.
Außerdem gehört jede Hanchan zu einer Veranstaltung.
"""
event = models.ForeignKey(Event)
event = models.ForeignKey(Event, on_delete=models.CASCADE)
start = models.DateTimeField(
_('Start'),
help_text=_('This is crucial to get the right Hanchans that scores')
@@ -94,7 +95,7 @@ class Hanchan(models.Model):
player1 = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
on_delete=models.PROTECT,
related_name='user_hanchan+',
verbose_name=_('Player 1'))
player1_input_score = models.IntegerField(_('Score'))
@@ -113,7 +114,7 @@ class Hanchan(models.Model):
player2 = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
on_delete=models.PROTECT,
related_name='user_hanchan+',
verbose_name=_('Player 2'))
player2_input_score = models.IntegerField(_('Score'))
@@ -132,7 +133,7 @@ class Hanchan(models.Model):
player3 = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
on_delete=models.PROTECT,
related_name='user_hanchan+',
verbose_name=_('Player 3'))
player3_input_score = models.IntegerField(_('Score'))
@@ -151,7 +152,7 @@ class Hanchan(models.Model):
player4 = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
on_delete=models.PROTECT,
related_name='user_hanchan+',
verbose_name=_('Player 4'))
player4_input_score = models.IntegerField(_('Score'))
@@ -335,18 +336,24 @@ class KyuDanRanking(models.Model):
Im Gegensatz zum Ladder Ranking ist das nicht Saison gebunden.
Deswegen läuft es getrennt.
"""
user = models.OneToOneField(settings.AUTH_USER_MODEL)
user = models.OneToOneField(settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)
dan = models.PositiveSmallIntegerField(blank=True, null=True)
dan_points = models.PositiveIntegerField(default=0)
max_dan_points = models.PositiveIntegerField(default=0)
kyu = models.PositiveSmallIntegerField(default=10, blank=True, null=True)
kyu_points = models.PositiveIntegerField(default=0)
won_hanchans = models.PositiveIntegerField(default=0)
good_hanchans = models.PositiveIntegerField(default=0)
hanchan_count = models.PositiveIntegerField(default=0)
legacy_date = models.DateField(blank=True, null=True)
legacy_hanchan_count = models.PositiveIntegerField(default=0)
legacy_dan_points = models.PositiveIntegerField(default=0)
legacy_kyu_points = models.PositiveIntegerField(default=0)
legacy_dan = models.PositiveSmallIntegerField(blank=True, null=True)
legacy_dan_points = models.PositiveIntegerField(blank=True, null=True)
legacy_kyu = models.PositiveSmallIntegerField(blank=True, null=True)
legacy_kyu_points = models.PositiveIntegerField(blank=True, null=True)
legacy_hanchan_count = models.PositiveIntegerField(blank=True, null=True)
legacy_good_hanchans = models.PositiveIntegerField(blank=True, null=True)
legacy_won_hanchans = models.PositiveIntegerField(blank=True, null=True)
wins_in_a_row = models.PositiveIntegerField(default=0)
last_hanchan_date = models.DateTimeField(blank=True, null=True)
objects = managers.KyuDanRankingManager()
@@ -356,11 +363,22 @@ class KyuDanRanking(models.Model):
verbose_name = _(u'Kyū/Dan Ranking')
verbose_name_plural = _(u'Kyū/Dan Rankings')
def __unicode__(self):
if self.dan_points is not None:
@property
def rank(self):
if self.dan is not None:
return "{0:d}. Dan".format(self.dan)
else:
return "{0:d}. Kyū".format(self.kyu or 10)
@property
def points(self):
return self.dan_points if self.dan is not None else self.kyu_points
def __str__(self):
if self.dan is not None:
return u"%s - %d. Dan" % (self.user.username, self.dan or 1)
else:
return u"%s - %d. Kyu" % (self.user.username, self.kyu or 10)
return u"%s - %d. Kyū" % (self.user.username, self.kyu or 10)
def append_3_in_a_row_bonuspoints(self, hanchan):
u"""
@@ -368,12 +386,14 @@ class KyuDanRanking(models.Model):
das er einen Dan Rang aufsteigt. Dies wird als Kommentar abgespeichert,
um es besser nachvollziehen zu können.
"""
if self.dan and hanchan.placement == 1:
if not self.dan or not settings.DAN_3_WINS_IN_A_ROW:
return
if hanchan.placement == 1:
self.wins_in_a_row += 1
else:
self.wins_in_a_row = 0
if self.dan and self.wins_in_a_row >= 3 and self.dan < 9:
return
if self.wins_in_a_row >= 3 and self.dan < 9:
LOGGER.info(
'adding bonuspoints for 3 wins in a row for %s', self.user)
new_dan_rank = self.dan + 1
@@ -394,8 +414,8 @@ class KyuDanRanking(models.Model):
bonus_points, new_dan_rank)
self.dan_points += bonus_points
self.wins_in_a_row = 0
self.update_rank()
# TODO: Komplett Überabreiten!
def append_tournament_bonuspoints(self, hanchan):
"""
Prüft ob es die letzte Hanchan in einem Turnier war. Wenn ja werden
@@ -408,29 +428,30 @@ class KyuDanRanking(models.Model):
user=self.user, event=hanchan.event
).order_by('-start')
last_hanchan_this_event = hanchans_this_event[0]
# Das braucht nur am Ende eines Turnieres gemacht werden.
if hanchan != last_hanchan_this_event:
# Das braucht nur am Ende eines Turnieres gemacht werden.
return False
else:
event_ranking = EventRanking.objects.get(
user=self.user,
event=hanchan.event
)
if event_ranking.placement == 1:
bonus_points += 4
hanchan.player_comment += u'+4 Punkte Turnier gewonnen. '
if event_ranking.avg_placement == 1:
bonus_points += 8
hanchan.player_comment += u'+8 Pkt: alle Spiele des Turnieres gewonnen. '
event_ranking = EventRanking.objects.get(
user=self.user,
event=hanchan.event
)
if event_ranking.placement == 1:
bonus_points += settings.TOURNAMENT_WIN_BONUSPOINTS
hanchan.player_comment += u'+{0:d} Punkte Turnier gewonnen. '.format(
settings.TOURNAMENT_WIN_BONUSPOINTS)
if event_ranking.avg_placement == 1:
bonus_points += settings.TOURNAMENT_FLAWLESS_VICTORY_BONUSPOINTS
hanchan.player_comment += u'+{0:d} Pkt: alle Spiele des Turnieres gewonnen. '.format(
settings.TOURNAMENT_FLAWLESS_VICTORY_BONUSPOINTS)
if bonus_points and self.dan:
hanchan.dan_points += bonus_points
self.dan_points += bonus_points
elif bonus_points:
hanchan.kyu_points += bonus_points
self.kyu_points += bonus_points
hanchan.bonus_points += bonus_points
return True
if bonus_points and self.dan:
hanchan.dan_points += bonus_points
self.dan_points += bonus_points
elif bonus_points:
hanchan.kyu_points += bonus_points
self.kyu_points += bonus_points
hanchan.bonus_points += bonus_points
return True
def get_absolute_url(self):
if self.dan or self.dan_points > 0:
@@ -451,20 +472,19 @@ class KyuDanRanking(models.Model):
force_recalc = True
if force_recalc:
# Setze alles auf die legacy Werte und berechne alles von neuem.
self.dan = None
self.dan = self.legacy_dan
self.dan_points = self.legacy_dan_points or 0
self.kyu = None
self.max_dan_points = self.dan_points
self.kyu = self.legacy_kyu
self.kyu_points = self.legacy_kyu_points or 0
self.hanchan_count = self.legacy_hanchan_count or 0
self.good_hanchans = 0
self.won_hanchans = 0
self.update_rank()
self.good_hanchans = self.legacy_good_hanchans or 0
self.won_hanchans = self.legacy_won_hanchans or 0
self.last_hanchan_date = None
if self.legacy_date:
since = timezone.make_aware(
datetime.combine(self.legacy_date, time(0, 0, 0)))
else:
since = None
self.update_rank()
since = timezone.make_aware(datetime.combine(
self.legacy_date,
time(23, 59, 59))) if self.legacy_date else None
elif self.last_hanchan_date:
since = self.last_hanchan_date
elif self.legacy_date:
@@ -479,42 +499,29 @@ class KyuDanRanking(models.Model):
valid_hanchans = valid_hanchans.filter(start__gt=since)
if until:
valid_hanchans = valid_hanchans.filter(start__lte=until)
self.hanchan_count += valid_hanchans.count()
for hanchan in valid_hanchans:
self.hanchan_count += 1
hanchan.get_playerdata(self.user)
if since and hanchan.start < since:
print(hanchan, "<", since, "no recalc")
LOGGER.debug(hanchan, "<", since, "no recalc")
self.dan_points += hanchan.dan_points or 0
self.kyu_points += hanchan.kyu_points or 0
self.update_rank()
else:
hanchan.bonus_points = 0
hanchan.player_comment = u""
hanchan.player_comment = ""
self.update_hanchan_points(hanchan)
if hanchan.event.mahjong_tournament:
self.append_tournament_bonuspoints(hanchan)
self.update_rank()
self.append_3_in_a_row_bonuspoints(hanchan)
self.update_rank()
hanchan.update_playerdata(self.user)
hanchan.save(recalculate=False)
self.won_hanchans += 1 if hanchan.placement == 1 else 0
self.good_hanchans += 1 if hanchan.placement == 2 else 0
self.last_hanchan_date = hanchan.start
LOGGER.debug(
'id: %(id)d, start: %(start)s, placement: %(placement)d, '
'score: %(score)d, kyu points: %(kyu_points)d, dan points: '
'%(dan_points)d, bonus points: %(bonus_points)d',
{'id': hanchan.pk, 'start': hanchan.start,
'placement': hanchan.placement, 'score': hanchan.game_score,
'kyu_points': hanchan.kyu_points or 0,
'dan_points': hanchan.dan_points or 0,
'bonus_points': hanchan.bonus_points or 0}
)
self.won_hanchans += 1 if hanchan.placement == 1 else 0
self.good_hanchans += 1 if hanchan.placement == 2 else 0
self.last_hanchan_date = hanchan.start
self.save(force_update=True)
def update_hanchan_points(self, hanchan):
"""
Berechne die Kyu bzw. Dan Punkte für eine Hanchan neu.
@@ -523,7 +530,7 @@ class KyuDanRanking(models.Model):
"""
hanchan.kyu_points = None
hanchan.dan_points = None
if hanchan.event.mahjong_tournament:
if hanchan.event.mahjong_tournament and settings.TOURNAMENT_POINT_SYSTEM:
"""Für Turniere gelten andere Regeln zur Punktevergabe:
1. Platz 4 Punkte
2. Platz 3 Punkte
@@ -547,6 +554,7 @@ class KyuDanRanking(models.Model):
hanchan.dan_points = -1
elif hanchan.placement == 4:
hanchan.dan_points = -2
# otherwise player must be in the kyu ranking
elif hanchan.game_score >= 60000:
hanchan.kyu_points = 3
elif hanchan.game_score >= 30000:
@@ -560,46 +568,45 @@ class KyuDanRanking(models.Model):
if self.dan:
# Only substract so much points that player has 0 Points:
if self.dan_points + hanchan.dan_points < 0:
hanchan.player_comment = 'Spieler unterschreitet 0 Punkte.' \
'(Original {} Punkte)'.format(hanchan.dan_points)
hanchan.dan_points -= (self.dan_points + hanchan.dan_points)
self.dan_points += hanchan.dan_points
else:
# Only substract so much points that player has 0 Points:
if self.kyu_points + hanchan.kyu_points < 0:
hanchan.player_comment = 'Spieler unterschreitet 0 Punkte.' \
'(Original {} Punkte)'.format(hanchan.kyu_points)
hanchan.kyu_points -= (self.kyu_points + hanchan.kyu_points)
self.kyu_points += hanchan.kyu_points
# TODO: Merkwürdige Methode die zwar funktioniert aber nicht sehr
# aussagekräfig ist. Überarbeiten?
def update_rank(self):
if self.dan and self.dan_points < 0:
self.dan_points = 0
self.dan = 1
elif self.dan or self.dan_points > 0:
old_dan = self.dan
for min_points, dan_rank in settings.DAN_RANKS:
if self.dan_points > min_points:
self.dan = dan_rank
break
if old_dan is None or self.dan > old_dan:
self.wins_in_a_row = 0
elif self.kyu_points < 1:
self.kyu_points = 0
self.kyu = 10
# Update Dan ranking:
if self.dan or self.dan_points > 0:
if settings.DAN_ALLOW_DROP_DOWN:
self.dan = max((dan for min_points, dan in settings.DAN_RANKS
if self.dan_points > min_points))
else:
self.max_dan_points = max(self.max_dan_points, self.dan_points)
self.dan = max((dan for min_points, dan in settings.DAN_RANKS
if self.max_dan_points > min_points))
# jump from Kyu to Dan
elif self.kyu_points > 50:
self.dan = 1
self.kyu = 0
self.dan_points = 0
self.kyu = None
self.kyu_points = 0
self.wins_in_a_row = 0
# update Kyu ranking_
else:
for min_points, kyu_rank in settings.KYU_RANKS:
if self.kyu_points > min_points:
self.kyu = kyu_rank
break
self.kyu = min((kyu for min_points, kyu in settings.KYU_RANKS
if self.kyu_points > min_points))
class SeasonRanking(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
user = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.PROTECT)
season = models.PositiveSmallIntegerField(_('Season'))
placement = models.PositiveIntegerField(blank=True, null=True)
avg_placement = models.FloatField(blank=True, null=True)

View File

@@ -0,0 +1,120 @@
{% extends "events/event_detail.html" %}{% load i18n humanize thumbnail %}
{% block title %}Hanchans: {{ event.name }}{% endblock %}
{% block extra_head %}
<script type="text/javascript" src="{{ STATIC_URL }}js/jquery.min.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}js/jquery.formset.js"></script>
{% endblock %}
{% block maincontent %}
<h2 class="grid_12">{% trans 'Edit Hanchans' %}</h2>
<form method="post" action="" id="eventhanchan_form">
{% csrf_token %}
{{ formset.management_form }}
{% for form in formset %}
<fieldset class="hanchan">
{% for hidden in form.hidden_fields %} {{ hidden }} {% endfor %}
<p>
<label for="id_{{ form.start.html_name }}_0" class="field_name {{ form.start.css_classes }}">{{ form.start.label }}:</label>
{{ form.start }}
{{ form.start.errors }}
</p>
<table>
<thead>
<tr>
<th>{{form.player1.label}}</th>
<th>{{form.player2.label}}</th>
<th>{{form.player3.label}}</th>
<th>{{form.player4.label}}</th>
</tr>
</thead>
<tr>
<td>{{ form.player1 }}</td>
<td>{{ form.player2 }}</td>
<td>{{ form.player3 }}</td>
<td>{{ form.player4 }}</td>
</tr>
<tr>
<td>{{ form.player1_input_score }}</td>
<td>{{ form.player2_input_score }}</td>
<td>{{ form.player3_input_score }}</td>
<td>{{ form.player4_input_score }}</td>
</tr>
</table>
<p>
<label class="field_name {{ form.comment.css_classes }}">{% trans 'Total' %}:</label>
<input type="number" value="0" name="total_score" disabled>
<label>{% trans 'Difference' %}:</label>
<span class="difference"></span>
</p>
<p><label for="id_{{ form.comment.html_name }}" class="field_name {{ form.comment.css_classes }}">{{ form.comment.label }}:</label>
{{ form.comment }}
{{ form.comment.errors }}
</p>
{% if form.instance.pk %}
<p>
<label for="id_{{ form.DELETE.html_name }}" class="field_name {{ form.DELETE.css_classes }}">{{ form.DELETE.label }}:</label>
{{ form.DELETE }} {{form.DELETE.help_text}}
{{ form.DELETE.errors }}
</p>
{% endif %}
{% if form.non_field_errors %}
<p> {{ form.non_field_errors }}</p>
{% endif %}
</fieldset>
{% endfor %}
</form>
<script type="text/javascript">
function autofill(row) {
row.find("input[id$='start_0']").val('{{ event.start|date:"SHORT_DATE_FORMAT"}}');
row.find("input[id$='start_1']").val('{{ event.start|time:'TIME_FORMAT'}}');
}
function recalculate_score(element) {
var difference = 100000
var total = 0;
score_fields = $(element).closest('fieldset').find('input[name$="input_score"]')
total_field = $(element).closest('fieldset').find('input[name$="total_score"]')
difference_field = $(element).closest('fieldset').find('span[class="difference"]')
score_fields.each(function() {total += Number($(this).val());});
total_field.val(total)
difference = 100000 - total
if (difference > 0) {
differnence_text = difference + ' offen'
} else if (difference < 0) {
differnence_text = (0 - difference) + ' zu viel'
} else {
differnence_text = 'Ok'
}
difference_field.text(differnence_text)
}
$(function() {
$('.hanchan').formset({
prefix: '{{ formset.prefix }}',
added: autofill,
addText: '<span class="fa fa-plus-circle"></span> {% trans 'Add Hanchan' %}',
addCssClass: 'button',
deleteText:'<span class="fa fa-trash"></span> {% trans 'Delete Hanchan' %}',
deleteCssClass: 'button'
});
})
$('input[name$="_input_score"]').change(function() {recalculate_score(this);});
$('input[name$="_input_score"]').keyup(function() {recalculate_score(this);});
$('input[name$="total_score"]').each(function() {recalculate_score(this);});
</script>
{% endblock %}
{% block comments %}{% endblock %}
{% block buttonbar %}
<a class="button" href="{% url 'event-hanchan-list' event.pk %}"><span class="fa fa-undo"></span> {% trans 'back' %}</a>
<button type="submit" form="eventhanchan_form"><span class="fa fa-hdd-o"></span> {% trans 'save' %}</button>
{% endblock %}

View File

@@ -52,6 +52,7 @@
{% block buttonbar %}
{% if perms.mahjong_ranking.add_hanchan %}
<a class="button" href="{{event.get_edit_url}}"><span class="fa fa-pencil"></span> {% trans 'Edit Event' %}</a>
<a class="button" href="{% url 'event-hanchan-form' event.id %}"><span class="fa fa-pencil"></span> {% trans 'Edit Hanchans' %}</a>
<a class="button" href="{% url 'add-hanchan-form' event.id %}"><span class="fa fa-plus-circle"></span> {% trans 'Add Hanchan' %}</a>
{% endif %}
{% endblock %}
{% endblock %}

View File

@@ -20,11 +20,6 @@
{% trans 'Nickname' %}
<a href="{% url 'kyudanranking-list' order_by='-username' %}?page={{page_obj.number}}" class="fa fa-sort-desc" rel="nofollow"></a>
</th>
<th>
<a href="{% url 'kyudanranking-list' order_by='+full_name'%}?page={{page_obj.number}}" class="fa fa-sort-asc" rel="nofollow"></a>
{% trans 'Full Name' %}
<a href="{% url 'kyudanranking-list' order_by='-full_name' %}?page={{page_obj.number}}" class="fa fa-sort-desc" rel="nofollow"></a>
</th>
<th>
<a href="{% url 'kyudanranking-list' order_by='+rank' %}?page={{page_obj.number}}" class="fa fa-sort-asc" rel="nofollow"></a>
{% trans 'Rank' %}
@@ -51,7 +46,6 @@
width="70" height="70" alt="" /></a>
</td>
<td><a href="{{ ranking.get_absolute_url }}">{{ ranking.user }}</a></td>
<td>{% if user.is_authenticated %}{{ranking.user.last_name}} {{ranking.user.first_name}}{% else %}---{% endif %}</td>
<td>{% if ranking.dan %} {{ranking.dan}}. Dan {% else %} {{ranking.kyu}} Kyu {% endif %}</td>
<td class="right">{% if ranking.dan %} {{ranking.dan_points}} {% else %} {{ranking.kyu_points}} {% endif %}</td>
<td class="right">{{ranking.hanchan_count}}</td>

View File

@@ -5,7 +5,7 @@
{% for hanchan in latest_hanchan_list %}
<li><span class="fa-li fa fa-table"></span>
<a href="{% url 'event-hanchan-list' hanchan.event_id %}">
<time datetime="{{ hanchan.start|date:'c' }}">{{ hanchan.start|date:'D' }}
<time datetime="{{ hanchan.start|date:'Y-m-d\TH:i:sO' }}">{{ hanchan.start|date:'D' }}
{{ hanchan.start|date:'SHORT_DATE_FORMAT' }} {{hanchan.start|time:'H:i'}}
</time></a>:<br />
<small>{{hanchan.player_names}}</small>
@@ -18,7 +18,7 @@
<li>
<span class="fa-li fa fa-calendar-o"></span>
<a href="{% url 'event-hanchan-list' event.pk %}">
<time datetime="{{event.start|date:'c'}}">{{ event.start|date:'D' }} {{ event.start|date:'SHORT_DATE_FORMAT' }}</time>:
<time datetime="{{event.start|date:'Y-m-d\TH:i:sO'}}">{{ event.start|date:'D' }} {{ event.start|date:'SHORT_DATE_FORMAT' }}</time>:
{{event.name}}</a></li>
{% endfor %}
</ul>

View File

@@ -50,4 +50,12 @@
</tr>
{% endfor %}
</table>
{% endblock %}
{% if kyu_dan_ranking.legacy_date %}
<p><strong>Frühere Dan Punkte vom {{ kyu_dan_ranking.legacy_date|date }}:</strong> {{kyu_dan_ranking.legacy_dan_points }}</p>
{% endif %}
{% endblock %}
{% block buttonbar %}
<a href="?download=xlsx" class="button"><span class="fa fa-table"></span> Download</a>
{% endblock %}

View File

@@ -35,8 +35,12 @@
{% if perms.mahjong_ranking.change_hanchan %}
<a href="{% url 'edit-hanchan' hanchan.pk %}"><span class="fa fa-pencil" title="{% trans 'Edit Hanchan' %}"></span></a>
{% endif %}
</td>
</td>
</tr>
{% endfor %}
{% endfor %}
</table>
{% endblock %}
{% endblock %}
{% block buttonbar %}
<a href="?download=xlsx" class="button"><span class="fa fa-table"></span> Download</a>
{% endblock %}

View File

@@ -16,7 +16,7 @@
<th rowspan="2">{% trans 'Placement' %}</th>
<th colspan="4">{% trans 'Players' %}</th>
<th rowspan="2">{% trans 'Kyu Points' %}</th>
<th rowspan="2"></th>
<th rowspan="2"></th>
</tr>
<tr>
<th>1.</th>
@@ -45,6 +45,10 @@
{% endif %}
</td>
</tr>
{% endfor %}
{% endfor %}
</table>
{% endblock %}
{% endblock %}
{% block buttonbar %}
<a href="?download=xlsx" class="button"><span class="fa fa-table"></span> Download</a>
{% endblock %}

View File

@@ -72,5 +72,8 @@
</form>
</td></tr></tfoot>
</table>
{% endblock %}
{% endblock %}
{% block buttonbar %}
<a href="?download=xlsx" class="button"><span class="fa fa-table"></span> Download</a>
{% endblock %}

View File

@@ -23,7 +23,6 @@
<th rowspan="2">{% trans "Rank" %}</th>
<th rowspan="2">{% trans "Avatar" %}</th>
<th rowspan="2">{% trans "Nickname" %}</th>
<th rowspan="2">{% trans "Name" %}</th>
<th colspan="2">{% trans 'Average' %}</th>
<th colspan="3">Hanchans</th>
</tr>
@@ -42,7 +41,6 @@
src="{% thumbnail player.user.avatar|default:'unknown_profile.jpg' 'avatar' %}"
width="70" height="70" class="avatar" alt=""/></a></td>
<td><a href="{{ player.get_absolute_url }}?season={{season}}">{{player.user}}</a></td>
<td>{% if user.is_authenticated %}{{player.user.last_name}} {{player.user.first_name}}{% else %}---{% endif %}</td>
<td class="center">{{player.avg_placement|floatformat:2 }}</td>
<td class="right">{{player.avg_score|floatformat:0|intcomma }}</td>
<td class="right">{{player.hanchan_count}}</td>
@@ -51,10 +49,10 @@
</tr>
{% empty %}
<tr>
<td colspan="9">Leider hat es noch niemand in das Ranking geschafft.
<td colspan="8">Leider hat es noch niemand in das Ranking geschafft.
Ein Spieler wird erst ins Ranking genommen wenn er 5 Hanchans absolviert hat.
</td>
</tr>
{% endfor %}
</table>
{% endblock %}
{% endblock %}

View File

@@ -4,14 +4,15 @@ from django.conf.urls import url
from django.views.generic import RedirectView
from . import views
urlpatterns = [ # Ignore PyLintBear (C0103)
url(r'^$',
RedirectView.as_view(url='/ranking/mahjong-ladder/', permanent=True)),
url(r'^event/(?P<event>[\d]+)/mahjong/$',
views.EventHanchanList.as_view(), name="event-hanchan-list"),
url(r'^event/(?P<event>[\d]+)/add-hanchan/$',
views.HanchanForm.as_view(), name="add-hanchan-form"),
url(r'^event/(?P<event>[\d]+)/edit/$',
views.EventHanchanForm.as_view(), name="event-hanchan-form"),
url(r'^event/(?P<event>[\d]+)/mahjong/$',
views.EventHanchanList.as_view(), name="event-hanchan-list"),
url(r'^event/(?P<event>[\d]+)/mahjong-ranking/$',
views.EventRankingList.as_view(), name="event-ranking"),
url(r'^hanchan/(?P<hanchan>[\d]+)/edit/$',
@@ -32,6 +33,6 @@ urlpatterns = [ # Ignore PyLintBear (C0103)
views.PlayerLadderScore.as_view(), name="player-ladder-score"),
url(r'^mahjong/$', views.KyuDanRankingList.as_view(),
name="kyudanranking-list"),
url(r'^mahjong/(?P<order_by>[\+\-\w]+)/$',
url(r'^mahjong/(?P<order_by>[\+\-][a-z_]+)/$',
views.KyuDanRankingList.as_view(), name="kyudanranking-list"),
]

View File

@@ -7,14 +7,16 @@ from django.contrib import auth
from django.contrib.auth.mixins import LoginRequiredMixin, \
PermissionRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.core.urlresolvers import reverse
from django.urls import reverse
from django.utils.translation import ugettext as _
from django.views import generic
from events.mixins import EventDetailMixin
from kasu import xlsx
from . import forms, models
from .mixins import MahjongMixin
DEFAULT_KYU_DAN_ORDER = '-score'
KYU_DAN_ORDER = { # map sort URL args to Django ORM order_by args
'+full_name': ('user__last_name', 'user__first_name'),
'-full_name': ('-user__last_name', '-user__first_name'),
@@ -29,6 +31,18 @@ KYU_DAN_ORDER = { # map sort URL args to Django ORM order_by args
}
def get_kyu_dan_ranking(user=None):
"""
get the KyuDanRanking from the Database, or return a blank one
:param user: user model
:return: KyuDanRanking object
"""
try:
return models.KyuDanRanking.objects.get(user=user)
except models.KyuDanRanking.DoesNotExist:
return models.KyuDanRanking(user=user)
class DeleteHanchan(EventDetailMixin, PermissionRequiredMixin,
generic.DeleteView):
"""Deletes a Hanchan if confimration has been answerd with 'yes'."""
@@ -106,6 +120,46 @@ class HanchanForm(SuccessMessageMixin, EventDetailMixin,
'one.') % self.object
class EventHanchanForm(EventDetailMixin, PermissionRequiredMixin,
generic.TemplateView):
"""Display a Formset to add and Edit Hanchans of the specific Event."""
permission_required = 'mahjong_ranking.add_hanchan'
template_name = 'mahjong_ranking/eventhanchan_form.html'
model = models.Hanchan
def get_context_data(self, **kwargs):
self.event = models.Event.objects.get(pk=self.kwargs['event'])
context = super(EventHanchanForm, self).get_context_data()
context['formset'] = self.formset
return context
def get(self, request, *args, **kwargs):
self.get_queryset()
self.formset = forms.HanchanFormset(
instance=self.event,
initial=[{'start': self.event.start}]
)
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
print("ICH WURDE GEPOSTET!!!!")
self.get_queryset()
self.formset = forms.HanchanFormset(
self.request.POST,
self.request.FILES,
instance=self.event,
initial=[{'start': self.event.start}]
)
if self.formset.is_valid():
self.formset.save()
return django.http.HttpResponseRedirect(
reverse('event-hanchan-form', kwargs={'event': self.event.pk})
)
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
class EventHanchanList(EventDetailMixin, generic.ListView):
"List all hanchans played on a given event."
model = models.Hanchan
@@ -117,23 +171,19 @@ class EventRankingList(EventDetailMixin, generic.ListView):
model = models.EventRanking
class KyuDanRankingList(MahjongMixin, generic.ListView):
"""List all Players with an Kyu or Dan score. """
default_order = '-score'
order_by = ''
order_by = None
paginate_by = 25
def dispatch(self, request, *args, **kwargs):
"""Set the order_by settings, revert to default_order if necessary."""
self.order_by = KYU_DAN_ORDER[
kwargs.get('order_by', self.default_order)
]
if kwargs.get('order_by') in KYU_DAN_ORDER.keys():
self.order_by = KYU_DAN_ORDER[kwargs.get('order_by')]
else:
self.order_by = KYU_DAN_ORDER[DEFAULT_KYU_DAN_ORDER]
return super(KyuDanRankingList, self).dispatch(request, *args, **kwargs)
def get_queryset(self):
queryset = models.KyuDanRanking.objects.filter(
hanchan_count__gt=0).order_by(*self.order_by)
@@ -160,7 +210,6 @@ class PlayerScore(LoginRequiredMixin, generic.ListView):
def get(self, request, *args, **kwargs):
user_model = auth.get_user_model()
username = kwargs.get('username')
try:
self.user = user_model.objects.get(
username=self.kwargs.get('username'))
@@ -168,6 +217,8 @@ class PlayerScore(LoginRequiredMixin, generic.ListView):
raise django.http.Http404(
_("No user found matching the name {}").format(
self.kwargs.get('username')))
if request.GET.get('download') == 'xlsx':
return self.get_xlsx(request, *args, **kwargs)
return super(PlayerScore, self).get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
@@ -186,20 +237,76 @@ class PlayerScore(LoginRequiredMixin, generic.ListView):
context['ladder_ranking'] = models.SeasonRanking(user=self.user)
return context
def get_xlsx(self, request, *args, **kwargs):
self.object_list = self.get_queryset()
response = django.http.HttpResponse(
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
response['Content-Disposition'] = 'attachment; ' \
'filename="{xlsx_filename}"'.format(
xlsx_filename=self.xlsx_filename)
xlxs_workbook = xlsx.Workbook()
xlxs_workbook.generate_sheet(
title=self.xlsx_filename.split('.')[0],
columns_settings=self.xlsx_columns,
object_list=self.object_list
)
xlxs_workbook.save(response)
return response
class PlayerDanScore(PlayerScore):
template_name = 'mahjong_ranking/player_dan_score.html'
def get_queryset(self):
kyu_dan_ranking = models.KyuDanRanking.objects.get(user=self.user)
return models.Hanchan.objects.dan_hanchans(user=self.user,
since=kyu_dan_ranking.legacy_date)
self.kyu_dan_ranking = get_kyu_dan_ranking(user=self.user)
return models.Hanchan.objects.dan_hanchans(
user=self.user,
since=self.kyu_dan_ranking.legacy_date)
@property
def xlsx_columns(self):
return (
{'col': 'A', 'title': 'Beginn', 'attr': 'start',
'style': 'Date Time',
'width': 14, 'footer': self.kyu_dan_ranking.legacy_date},
{'col': 'B', 'title': 'Termin', 'attr': 'event.name',
'style': 'Content', 'width': 16},
{'col': 'C', 'title': 'Platzierung', 'attr': 'placement',
'style': 'Integer', 'width': 11},
{'col': 'D', 'title': 'Spieler 1', 'attr': 'player1.username',
'style': 'Content', 'width': 16},
{'col': 'E', 'title': 'Punkte', 'attr': 'player1_game_score',
'style': 'Integer', 'width': 8},
{'col': 'F', 'title': 'Spieler 2', 'attr': 'player2.username',
'style': 'Content', 'width': 16},
{'col': 'G', 'title': 'Punkte', 'attr': 'player2_game_score',
'style': 'Integer', 'width': 8},
{'col': 'H', 'title': 'Spieler 3', 'attr': 'player3.username',
'style': 'Content', 'width': 16},
{'col': 'I', 'title': 'Punkte', 'attr': 'player3_game_score',
'style': 'Integer', 'width': 8},
{'col': 'J', 'title': 'Spieler 4', 'attr': 'player4.username',
'style': 'Content', 'width': 16},
{'col': 'K', 'title': 'Punkte', 'attr': 'player4_game_score',
'style': 'Integer', 'width': 8},
{'col': 'L', 'title': 'Dan Punkte', 'attr': 'dan_points',
'style': 'Integer', 'width': 12,
'footer': self.kyu_dan_ranking.legacy_dan_points},
{'col': 'M', 'title': 'Anmerkung', 'attr': 'player_comment',
'style': 'Content', 'width': 20, 'footer': 'Legacy Dan Punkte'},
)
@property
def xlsx_filename(self):
return "{username}_dan_score.xlsx".format(username=self.user.username)
class PlayerInvalidScore(PlayerScore):
template_name = 'mahjong_ranking/player_invalid_score.html'
def get_queryset(self):
self.xlsx_filename = "{username}_invalid_score.xlsx".format(
username=self.user.username)
return models.Hanchan.objects.unconfirmed(user=self.user)
@@ -207,7 +314,47 @@ class PlayerKyuScore(PlayerScore):
template_name = 'mahjong_ranking/player_kyu_score.html'
def get_queryset(self):
return models.Hanchan.objects.kyu_hanchans(self.user)
self.kyu_dan_ranking = get_kyu_dan_ranking(user=self.user)
return models.Hanchan.objects.kyu_hanchans(
user=self.user,
since=self.kyu_dan_ranking.legacy_date)
@property
def xlsx_columns(self):
return (
{'col': 'A', 'title': 'Beginn', 'attr': 'start',
'style': 'Date Time',
'width': 14, 'footer': self.kyu_dan_ranking.legacy_date},
{'col': 'B', 'title': 'Termin', 'attr': 'event.name',
'style': 'Content', 'width': 16},
{'col': 'C', 'title': 'Platzierung', 'attr': 'placement',
'style': 'Integer', 'width': 11},
{'col': 'D', 'title': 'Spieler 1', 'attr': 'player1.username',
'style': 'Content', 'width': 16},
{'col': 'E', 'title': 'Punkte', 'attr': 'player1_game_score',
'style': 'Integer', 'width': 8},
{'col': 'F', 'title': 'Spieler 2', 'attr': 'player2.username',
'style': 'Content', 'width': 16},
{'col': 'G', 'title': 'Punkte', 'attr': 'player2_game_score',
'style': 'Integer', 'width': 8},
{'col': 'H', 'title': 'Spieler 3', 'attr': 'player3.username',
'style': 'Content', 'width': 16},
{'col': 'I', 'title': 'Punkte', 'attr': 'player3_game_score',
'style': 'Integer', 'width': 8},
{'col': 'J', 'title': 'Spieler 4', 'attr': 'player4.username',
'style': 'Content', 'width': 16},
{'col': 'K', 'title': 'Punkte', 'attr': 'player4_game_score',
'style': 'Integer', 'width': 8},
{'col': 'L', 'title': 'Kyū Punkte', 'attr': 'kyu_points',
'style': 'Integer', 'width': 12,
'footer': self.kyu_dan_ranking.legacy_kyu_points},
{'col': 'M', 'title': 'Anmerkung', 'attr': 'comment',
'style': 'Content', 'width': 24, 'footer': 'Legacy Kyū Punkte'},
)
@property
def xlsx_filename(self):
return "{username}_kyu_score.xlsx".format(username=self.user.username)
class PlayerLadderScore(PlayerScore):
@@ -229,3 +376,39 @@ class PlayerLadderScore(PlayerScore):
season=self.season
)
return hanchan_list
@property
def xlsx_columns(self):
return (
{'col': 'A', 'title': 'Beginn', 'attr': 'start',
'style': 'Date Time', 'width': 14},
{'col': 'B', 'title': 'Termin', 'attr': 'event.name',
'style': 'Content', 'width': 16},
{'col': 'C', 'title': 'Platzierung', 'attr': 'placement',
'style': 'Integer', 'width': 11},
{'col': 'D', 'title': 'Spieler 1', 'attr': 'player1.username',
'style': 'Content', 'width': 16},
{'col': 'E', 'title': 'Punkte', 'attr': 'player1_game_score',
'style': 'Integer', 'width': 8},
{'col': 'F', 'title': 'Spieler 2', 'attr': 'player2.username',
'style': 'Content', 'width': 16},
{'col': 'G', 'title': 'Punkte', 'attr': 'player2_game_score',
'style': 'Integer', 'width': 8},
{'col': 'H', 'title': 'Spieler 3', 'attr': 'player3.username',
'style': 'Content', 'width': 16},
{'col': 'I', 'title': 'Punkte', 'attr': 'player3_game_score',
'style': 'Integer', 'width': 8},
{'col': 'J', 'title': 'Spieler 4', 'attr': 'player4.username',
'style': 'Content', 'width': 16},
{'col': 'K', 'title': 'Punkte', 'attr': 'player4_game_score',
'style': 'Integer', 'width': 8},
{'col': 'L', 'title': 'Punkte', 'attr': 'game_score',
'style': 'Integer', 'width': 8},
)
@property
def xlsx_filename(self):
return "{username}_ladder_score_{season}.xlsx".format(
username=self.user.username,
season=self.season
)

View File

@@ -1 +1 @@
"""A simple ladder ranking system for the game mai-star."""
"""A simple ladder ranking system for the game mai-star."""

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: kasu.mahjong_ranking\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-04-27 09:49+0200\n"
"POT-Creation-Date: 2019-12-13 23:38+0100\n"
"PO-Revision-Date: 2016-09-28 00:24+0200\n"
"Last-Translator: Christian Berg <xeniac.at@gmail.com>\n"
"Language-Team: Kasu <verein@kasu.at>\n"
@@ -28,54 +28,54 @@ msgstr "Neuberechnen"
msgid "%s may only participate once."
msgstr "%s darf nur einmal teilnehmen."
#: models.py:20
#: models.py:21
msgid "Comment"
msgstr "Kommentar"
#: models.py:22
#: models.py:24
msgid "Player 1"
msgstr "Spieler 1"
#: models.py:24 models.py:30 models.py:36 models.py:42 models.py:48
#: models.py:54 templates/maistar_ranking/ranking_list.html:19
#: models.py:26 models.py:33 models.py:40 models.py:47 models.py:54
#: models.py:61 templates/maistar_ranking/ranking_list.html:19
msgid "Score"
msgstr "Punkte"
#: models.py:28
#: models.py:31
msgid "Player 2"
msgstr "Spieler 2"
#: models.py:34
#: models.py:38
msgid "Player 3"
msgstr "Spieler 3"
#: models.py:40
#: models.py:45
msgid "Player 4"
msgstr "Spieler 4"
#: models.py:46
#: models.py:52
msgid "Player 5"
msgstr "Spieler 5"
#: models.py:52
#: models.py:59
msgid "Player 6"
msgstr "Spieler 6"
#: models.py:58
#: models.py:65
msgid "Has been confirmed"
msgstr "Wurde bestätigt"
#: models.py:60
#: models.py:67
msgid "the game only counts whe it has been confirmed"
msgstr "das Spiel zählt nur wenn es bestätigt wurde"
#: models.py:63 models.py:147 templates/maistar_ranking/player_game_list.html:6
#: models.py:70 models.py:153 templates/maistar_ranking/player_game_list.html:6
#: templates/maistar_ranking/ranking_list.html:4
#: templates/maistar_ranking/ranking_list.html:72
msgid "Season"
msgstr "Saison"
#: models.py:74
#: models.py:80
msgid "Mai-Star Game with {0} from {1:%Y-%m-%d}"
msgstr "Mai-Star Spiel mit {0} vom {1:%Y-%m-%d}"

View File

@@ -37,19 +37,19 @@ class Migration(migrations.Migration):
('season', models.PositiveSmallIntegerField(
verbose_name='Saison', editable=False, db_index=True)),
('event', models.ForeignKey(
related_name='maistargame_set', to='events.Event')),
related_name='maistargame_set', to='events.Event', on_delete=models.CASCADE)),
('player1', models.ForeignKey(related_name='+',
verbose_name='Spieler 1', to=settings.AUTH_USER_MODEL)),
verbose_name='Spieler 1', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
('player2', models.ForeignKey(related_name='+',
verbose_name='Spieler 2', to=settings.AUTH_USER_MODEL)),
verbose_name='Spieler 2', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
('player3', models.ForeignKey(related_name='+',
verbose_name='Spieler 3', to=settings.AUTH_USER_MODEL)),
verbose_name='Spieler 3', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
('player4', models.ForeignKey(related_name='+',
verbose_name='Spieler 4', to=settings.AUTH_USER_MODEL)),
verbose_name='Spieler 4', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
('player5', models.ForeignKey(related_name='+',
verbose_name='Spieler 5', to=settings.AUTH_USER_MODEL)),
verbose_name='Spieler 5', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
('player6', models.ForeignKey(related_name='+',
verbose_name='Spieler 6', to=settings.AUTH_USER_MODEL)),
verbose_name='Spieler 6', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
],
),
migrations.CreateModel(
@@ -65,7 +65,7 @@ class Migration(migrations.Migration):
('games_count', models.PositiveSmallIntegerField(default=0)),
('games_good', models.PositiveSmallIntegerField(default=0)),
('games_won', models.PositiveSmallIntegerField(default=0)),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
],
options={
'ordering': ('-season', 'placement', 'avg_placement', '-avg_score'),

View File

@@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.8 on 2017-12-14 11:15
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('maistar_ranking', '0006_auto_20171115_0653'),
]
operations = [
migrations.AlterField(
model_name='game',
name='player1',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Spieler 1'),
),
migrations.AlterField(
model_name='game',
name='player2',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Spieler 2'),
),
migrations.AlterField(
model_name='game',
name='player3',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Spieler 3'),
),
migrations.AlterField(
model_name='game',
name='player4',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Spieler 4'),
),
migrations.AlterField(
model_name='game',
name='player5',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Spieler 5'),
),
migrations.AlterField(
model_name='game',
name='player6',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Spieler 6'),
),
migrations.AlterField(
model_name='ranking',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -2,11 +2,11 @@
import logging
from django.core.urlresolvers import reverse
from django.db import models
from django.db.models.signals import post_delete, post_save
from django.utils.translation import ugettext as _
from django.dispatch import receiver
from django.urls import reverse
from django.utils.translation import ugettext as _
from events.models import Event
from . import settings, managers
@@ -16,40 +16,47 @@ class Game(models.Model):
"""to record a complete game with 6 different players."""
_player_list = list()
event = models.ForeignKey(Event, related_name='maistargame_set')
event = models.ForeignKey(Event, on_delete=models.CASCADE,
related_name='maistargame_set')
comment = models.TextField(_('Comment'), blank=True)
player1 = models.ForeignKey(
settings.AUTH_USER_MODEL, verbose_name=_("Player 1"), related_name='+'
settings.AUTH_USER_MODEL, on_delete=models.PROTECT,
verbose_name=_("Player 1"), related_name='+'
)
player1_score = models.SmallIntegerField(_("Score"))
player1_placement = models.PositiveSmallIntegerField(editable=False)
player2 = models.ForeignKey(
settings.AUTH_USER_MODEL, verbose_name=_("Player 2"), related_name='+'
settings.AUTH_USER_MODEL, on_delete=models.PROTECT,
verbose_name=_("Player 2"), related_name='+'
)
player2_score = models.SmallIntegerField(_("Score"))
player2_placement = models.PositiveSmallIntegerField(editable=False)
player3 = models.ForeignKey(
settings.AUTH_USER_MODEL, verbose_name=_("Player 3"), related_name='+'
settings.AUTH_USER_MODEL, on_delete=models.PROTECT,
verbose_name=_("Player 3"), related_name='+'
)
player3_score = models.SmallIntegerField(_("Score"))
player3_placement = models.PositiveSmallIntegerField(editable=False)
player4 = models.ForeignKey(
settings.AUTH_USER_MODEL, verbose_name=_("Player 4"), related_name='+'
settings.AUTH_USER_MODEL, on_delete=models.PROTECT,
verbose_name=_("Player 4"), related_name='+'
)
player4_score = models.SmallIntegerField(_("Score"))
player4_placement = models.PositiveSmallIntegerField(editable=False)
player5 = models.ForeignKey(
settings.AUTH_USER_MODEL, verbose_name=_("Player 5"), related_name='+'
settings.AUTH_USER_MODEL, on_delete=models.PROTECT,
verbose_name=_("Player 5"), related_name='+'
)
player5_score = models.SmallIntegerField(_("Score"))
player5_placement = models.PositiveSmallIntegerField(editable=False)
player6 = models.ForeignKey(
settings.AUTH_USER_MODEL, verbose_name=_("Player 6"), related_name='+'
settings.AUTH_USER_MODEL, on_delete=models.PROTECT,
verbose_name=_("Player 6"), related_name='+'
)
player6_score = models.SmallIntegerField(_("Score"))
player6_placement = models.PositiveSmallIntegerField(editable=False)
@@ -69,7 +76,6 @@ class Game(models.Model):
"""Display rankings by placement, best players first."""
ordering = ('-event__start', '-id')
def __str__(self):
return _("Mai-Star Game with {0} from {1:%Y-%m-%d}").format(
self.player_names, self.event.start
@@ -143,7 +149,8 @@ class Game(models.Model):
class Ranking(models.Model):
"""the player scores in the ladder for one season. """
user = models.ForeignKey(settings.AUTH_USER_MODEL)
user = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.PROTECT)
season = models.PositiveSmallIntegerField(_("Season"))
placement = models.PositiveIntegerField(blank=True, null=True)
avg_placement = models.PositiveSmallIntegerField(blank=True, null=True)

View File

@@ -5,7 +5,6 @@ from django.urls import reverse
from events.models import Event
class MaistarGamesSitemap(GenericSitemap):
@staticmethod
def items():

View File

@@ -3,7 +3,7 @@
from datetime import date
from django.contrib import auth
from django.core.urlresolvers import reverse
from django.urls import reverse
from django.shortcuts import get_object_or_404
from django.views import generic

View File

@@ -1,11 +1,11 @@
""" Admin Interface to manage the memberships."""
# import stuff we need from django
from django.contrib import admin
from django.contrib import admin, messages
from django.contrib.auth.admin import UserAdmin, GroupAdmin
from django.contrib.auth.models import Group
from django.core.exceptions import PermissionDenied
from django.utils.translation import ugettext as _
from easy_thumbnails import fields, widgets
from membership.models import Membership, ActivationRequest
@@ -16,13 +16,14 @@ def activate_user(modeladmin, request, queryset):
:param request: An HttpRequest representing the current request.
:param queryset: A QuerySet containing the objects selected by the user.
"""
[activation.activate() for activation in queryset ]
[activation.activate() for activation in queryset]
activate_user.short_description = _('Activate selected User')
def cleanup_activation(modeladmin, request, queryset): # Ignore PyLintBear (W0613)
def cleanup_activation(modeladmin, request,
queryset): # Ignore PyLintBear (W0613)
"""Delete every selected activation request that has been expired.
:param modeladmin: The ModelAdmin that triggered this action.
@@ -38,6 +39,38 @@ cleanup_activation.short_description = _(
"Cleanup selected Activation Requests")
def clear_personal_data(modeladmin, request, queryset):
"""deactivates the account and removes all personal user information.
:param modeladmin: The ModelAdmin that triggered this action.
:param request: An HttpRequest representing the current request.
:param queryset: A QuerySet containing the objects selected by the user.
"""
cleared_memberships = 0
if not modeladmin.has_delete_permission(request):
raise PermissionDenied
for membership in queryset:
if membership.membership == False:
[setattr(membership, fieldname, None)
for fieldname in membership.nullable_personal_data]
[setattr(membership, fieldname, "")
for fieldname in membership.blankable_personal_data]
membership.is_active = False
membership.confirmed = False
membership.membership = False
membership.save()
cleared_memberships += 1
else:
modeladmin.message_user(request, _(
"Can't remove personal data from active member %s.") % membership.username, messages.ERROR)
if cleared_memberships > 0:
modeladmin.message_user(request, _(
"Cleared %d personal data profiles.") % cleared_memberships, messages.INFO)
clear_personal_data.short_description = _("Clear personal Data")
class ProxyGroup(Group):
"""A Proxy to list Usergroups from django.contrib.auth here."""
@@ -50,6 +83,8 @@ class ProxyGroup(Group):
class MembershipAdmin(UserAdmin):
"""Admin interface to manage membership. e.g. users."""
actions = [clear_personal_data, ]
formfield_overrides = {
fields.ThumbnailerImageField: {
'widget': widgets.ImageClearableFileInput},
@@ -62,9 +97,9 @@ class MembershipAdmin(UserAdmin):
'is_active',
'membership',
'confirmed',
'paid_until',
'last_login',
)
list_editable = ('confirmed', 'paid_until',)
list_editable = ('confirmed', )
list_display_links = ('username',)
fieldsets = (
(None, {'fields': (('username', 'password'), 'gender', 'avatar',

View File

@@ -3,16 +3,16 @@ Created on 03.10.2011
@author: Christian
"""
from datetime import date
from captcha.fields import ReCaptchaField
from django import forms
from django.conf import settings
from django.contrib import auth
from django.contrib.sites.models import Site
from django.utils.translation import ugettext_lazy as _
from utils.massmailer import MassMailer
from . import models
from content.models import Page
class MembershipForm(forms.ModelForm):
@@ -25,46 +25,32 @@ class MembershipForm(forms.ModelForm):
help_text=_('Input format: yyyy-mm-dd')
)
email = forms.EmailField(label=_('Email'), required=True)
required_membership_fields = ('first_name', 'last_name', 'birthday')
class Meta:
"""get the user model dyamicly"""
model = auth.get_user_model()
fields = (
'username', 'gender', 'first_name', 'last_name', 'email', 'avatar',
'website', 'membership', 'birthday', 'telephone', 'street_name',
'post_code', 'city'
'username', 'email', 'avatar',
'membership', 'first_name', 'last_name', 'birthday',
)
def clean_birthday(self):
"""If the user wants to be a member the birthday field is mandatory."""
if self.cleaned_data['membership'] \
and not self.cleaned_data['birthday']:
raise forms.ValidationError(_('For your membership, we need this. \
Please fill out this field yet.'))
return self.cleaned_data['birthday']
def clean_street_name(self):
"""If the user wants to be a member the address is mandatory."""
if self.cleaned_data['membership'] \
and not self.cleaned_data['street_name']:
raise forms.ValidationError(_('For your membership, we need this. \
Please fill out this field yet.'))
return self.cleaned_data['street_name']
def clean_post_code(self):
"""If the user wants to be a member the address is mandatory."""
if self.cleaned_data['membership'] \
and not self.cleaned_data['post_code']:
raise forms.ValidationError(_('For your membership, we need this. \
Please fill out this field yet.'))
return self.cleaned_data['post_code']
def clean_city(self):
"""If the user wants to be a member the address is mandatory."""
if self.cleaned_data['membership'] and not self.cleaned_data['city']:
raise forms.ValidationError(_('For your membership, we need this. \
Please fill out this field yet.'))
return self.cleaned_data['city']
def clean(self):
cleaned_data = super().clean()
errormsg = _('For your membership, we need this. \
Please fill out this field yet.')
membership = cleaned_data.get('membership')
for fieldname in self.required_membership_fields:
if membership and not cleaned_data.get(fieldname):
self.add_error(fieldname, errormsg)
if membership and cleaned_data.get('birthday'):
birthday = cleaned_data.get('birthday')
today = date.today()
age = today.year - birthday.year - (
(today.month, today.day) < (birthday.month, birthday.day))
if age < 16:
self.add_error('birthday',
'Midestalter für Mitlieder ist 16 Jahre!')
class RegistrationForm(MembershipForm):
@@ -78,15 +64,26 @@ class RegistrationForm(MembershipForm):
widget=forms.PasswordInput(), label=_('password'))
password2 = forms.CharField(
widget=forms.PasswordInput(), label=_('password (again)'))
privacy_policy = forms.BooleanField(
label=_('Privacy policy'),
help_text=_(
'I have read and understood the <a href="%s">privacy policy</a>') %
Page.objects.get(slug='privacypolicy').get_absolute_url())
membership = forms.BooleanField(
label=_('Membership'),
help_text=_(
'Yes, I confirm that I am in agreement with the '
'<a href="%s">statutes</a> and would like to become a member.') %
Page.objects.get(slug='statutes').get_absolute_url())
recaptcha = ReCaptchaField()
class Meta:
"""Metadata to localize and customize the ModelForm."""
model = auth.get_user_model()
fields = ('first_name', 'last_name', 'username', 'email',
'username', 'gender', 'first_name', 'last_name', 'email',
'avatar',
'website', 'membership', 'birthday', 'telephone',
'membership', 'birthday', 'telephone',
'street_name',
'post_code', 'city'
)
@@ -122,7 +119,7 @@ class RegistrationForm(MembershipForm):
def save(self, commit=True):
""" Create the new User, set him/her inactive, create an acitivation
request for the user and send him/her an activation email.
:param commit: commit the SQL and send the email if True
:return: the created User Object
"""

View File

@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: kasu.membership\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-04-27 09:49+0200\n"
"PO-Revision-Date: 2018-04-27 10:30+0105\n"
"POT-Creation-Date: 2019-12-13 23:38+0100\n"
"PO-Revision-Date: 2018-05-08 00:19+0105\n"
"Last-Translator: b'Christian Berg <kasu@xendynastie.at>'\n"
"Language-Team: Kasu <verein@kasu.at>\n"
"Language: de\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 1.8.9\n"
"X-Translated-Using: django-rosetta 0.7.14\n"
"X-Translated-Using: django-rosetta 0.8.1\n"
#: __init__.py:11
msgid "Male"
@@ -31,32 +31,46 @@ msgstr "Weiblich"
msgid "Activate selected User"
msgstr "Ausgewählte Benutzer freischalten"
#: admin.py:38
#: admin.py:39
msgid "Cleanup selected Activation Requests"
msgstr "Ausgewählte Aktivierungsanfragen bereinigen"
#: admin.py:47
#: admin.py:62
#, python-format
msgid "Can't remove personal data from active member %s."
msgstr ""
"Persönliche Daten von aktiven Mitglied %s können nicht entfernt werden."
#: admin.py:64
#, python-format
msgid "Cleared %d personal data profiles."
msgstr "Persönliche Daten in %d Profilen entfernt."
#: admin.py:66
msgid "Clear personal Data"
msgstr "Persönliche Daten bereinigen"
#: admin.py:75
msgid "Group"
msgstr "Gruppe"
#: admin.py:48
#: admin.py:76
msgid "Groups"
msgstr "Gruppen"
#: admin.py:72 models.py:162 models.py:215
#: templates/membership/register_form.html:32
#: admin.py:102 forms.py:73 models.py:163 models.py:218
msgid "Membership"
msgstr "Mitgliedschaft"
#: admin.py:77
#: admin.py:107
msgid "Permissions"
msgstr "Berechtigung"
#: admin.py:79
#: admin.py:109
msgid "Important dates"
msgstr "Wichtige Daten"
#: forms.py:23
#: forms.py:23 templates/membership/membership_detail.html:46
msgid "birthday"
msgstr "Geburtstag"
@@ -64,109 +78,121 @@ msgstr "Geburtstag"
msgid "Input format: yyyy-mm-dd"
msgstr "Eingabeformat: tt.mm.jjjj"
#: forms.py:27
#: forms.py:27 templates/membership/membership_detail.html:48
msgid "Email"
msgstr "E-Mail"
#: forms.py:42 forms.py:50 forms.py:58
#: forms.py:40
msgid ""
"For your membership, we need this. Please fill out this field "
"yet."
msgstr "Diese Angabe wird für eine Mitgliedschaft benötigt, bitte ausfüllen."
#: forms.py:65
msgid ""
"For your membership, we need this. Please fill out this field "
"yet."
msgstr "Diese Angabe wird für eine Mitgliedschaft benötigt, bitte ausfüllen."
#: forms.py:78
#: forms.py:64
msgid "password"
msgstr "Passwort"
#: forms.py:80
#: forms.py:66
msgid "password (again)"
msgstr "Passwort (wiederholen)"
#: forms.py:102
msgid "This username is already taken. Please choose another."
msgstr ""
"Diesen Benutzername ist schon vergeben. Bitte einen anderen auswählen."
#: forms.py:68
msgid "Privacy policy"
msgstr "Datenschutzerklärung"
#: forms.py:109
#: forms.py:70
#, python-format
msgid "I have read and understood the <a href=\"%s\">privacy policy</a>"
msgstr ""
"Ich habe die <a href=\"%s\"> Datenschutzerklärung</a> gelesen und verstanden"
#: forms.py:75
#, python-format
msgid ""
"Yes, I confirm that I am in agreement with the <a href=\"%s\">statutes</a> "
"and would like to become a member."
msgstr ""
"Ja, ich bin mit den <a href=\"%s\">Statuen</a> einverstanden und möchte "
"Mitglied werden."
#: forms.py:99
msgid "This username is already taken. Please choose another."
msgstr "Diesen Benutzername ist schon vergeben. Bitte einen anderen auswählen."
#: forms.py:106
msgid ""
"This email address is already in use. Please supply a different "
"email address."
msgstr "Die E-Mail Adresse wird schon verwendet. Bitte eine andere angeben."
#: forms.py:119
#: forms.py:116
msgid "The two password fields didn't match."
msgstr "Die beiden Passwörter passen nicht."
#: models.py:83
#: models.py:84
msgid "user"
msgstr "Benutzer"
#: models.py:85
#: models.py:86
msgid "activation key"
msgstr "Aktivierungsschlüssel"
#: models.py:89
#: models.py:90
msgid "pending activation"
msgstr "Ausstehende Aktivierung"
#: models.py:90
#: models.py:91
msgid "pending activations"
msgstr "Wartende Aktivierungen"
#: models.py:93
#: models.py:94
#, python-format
msgid "user registration for %s"
msgstr "Benutzerregistrierung für %s"
#: models.py:148
#: models.py:149
msgid "Gender"
msgstr "Geschlecht"
#: models.py:164
#: models.py:165
msgid ""
"Yes, I confirm that I am in agreement with the statutes and would "
"like to become a member."
msgstr "Ja, ich bin mit den Statuen einverstanden und möchte Mitglied werden."
#: models.py:168
#: models.py:169
msgid "Birthday Date"
msgstr "Geburtstag"
#: models.py:172
#: models.py:173 templates/membership/membership_detail.html:49
msgid "Telephone"
msgstr "Telefon"
#: models.py:178
#: models.py:179 templates/membership/membership_detail.html:47
msgid "Address"
msgstr "Adresse"
#: models.py:184
#: models.py:185
msgid "Postcode"
msgstr "Postleitzahl"
#: models.py:189
#: models.py:190
msgid "Town/City"
msgstr "Ort"
#: models.py:197
#: models.py:198
msgid "Paid until"
msgstr "Bezahlt bis"
#: models.py:203
#: models.py:204
msgid "Confirmed"
msgstr "Bestätigt"
#: models.py:205
#: models.py:206
msgid "This person has paid the membership fee."
msgstr "Diese Person hat ihre Mitgliedschaft bezahlt"
#: models.py:216
#: models.py:219
msgid "Memberships"
msgstr "Mitgliedschaften"
@@ -181,16 +207,20 @@ msgid ""
"We received an account request on %(site.domain)s for your email address.\n"
"To activate your account please visit the following link:"
msgstr ""
"Jemand (hoffentlich du selbst) möchte mit dieser E-Mail Adresse einen neuen Benutzer Account für %(site.domain)s anlegen.\n"
"Solltest du diesen Account aktivieren wollen, klicke bitte auf den unten stehenden Link, oder kopiere diesen in die Adresszeile deines Browsers:"
"Jemand (hoffentlich du selbst) möchte mit dieser E-Mail Adresse einen neuen "
"Benutzer Account für %(site.domain)s anlegen.\n"
"Solltest du diesen Account aktivieren wollen, klicke bitte auf den unten "
"stehenden Link, oder kopiere diesen in die Adresszeile deines Browsers:"
#: templates/membership/email/activation_email.txt:9
#, python-format
msgid ""
"If you do not want to open an account on %(site.domain)s, please ignore this email.\n"
"If you do not want to open an account on %(site.domain)s, please ignore this "
"email.\n"
"Your information will then be deleted in a few days time."
msgstr ""
"Wenn du keinen Zugang für %(site.domain)s eröffnen willst, ignoriere diese E-Mail bitte.\n"
"Wenn du keinen Zugang für %(site.domain)s eröffnen willst, ignoriere diese E-"
"Mail bitte.\n"
"Die Zugangsdaten werden dann in ein paar Tagen automatisch gelöscht."
#: templates/membership/email/activation_email.txt:12
@@ -267,81 +297,92 @@ msgstr "Anmerkung"
msgid "This Hanchan does not validate"
msgstr "Diese Hanchan ist ungültig"
#: templates/membership/membership_detail.html:6
#: templates/membership/membership_detail.html:5
msgid "profile for"
msgstr "Profil für"
#: templates/membership/membership_detail.html:10
#: templates/membership/membership_detail.html:9
msgid "Ladder Hanchans"
msgstr "Ladder Hanchans"
#: templates/membership/membership_detail.html:11
#: templates/membership/membership_detail.html:10
msgid "Kyu Hanchans"
msgstr "Kyū Hanchans"
#: templates/membership/membership_detail.html:12
#: templates/membership/membership_detail.html:11
msgid "Dan Hanchans"
msgstr "Dan Hanchans"
#: templates/membership/membership_detail.html:13
#: templates/membership/membership_detail.html:12
msgid "Invalid Hanchans"
msgstr "Ungültige Hanchans"
#: templates/membership/membership_detail.html:14
#: templates/membership/membership_detail.html:13
msgid "Mai-Star Games"
msgstr "Mai-Star Spiele"
#: templates/membership/membership_detail.html:20
#: templates/membership/membership_detail.html:17
msgid "Profile Image"
msgstr "Profilbild"
#: templates/membership/membership_detail.html:28
msgid "Member Since"
msgstr "Mitglied seit"
#: templates/membership/membership_detail.html:26
#: templates/membership/membership_detail.html:29
msgid "Last Login"
msgstr "Letzte Anmeldung"
#: templates/membership/membership_detail.html:38
#: templates/membership/membership_detail.html:40
msgid "Points"
msgstr "Punkte"
#: templates/membership/membership_detail.html:42
#: templates/membership/membership_detail.html:26
msgid "Maximum"
msgstr "Maximum"
#: templates/membership/membership_detail.html:32
msgid "Games Total"
msgstr "Spiele gesamt"
#: templates/membership/membership_detail.html:43
#: templates/membership/membership_detail.html:45
#: templates/membership/membership_detail.html:32
#: templates/membership/membership_detail.html:35
msgid "Won"
msgstr "Gewonnen"
#: templates/membership/membership_detail.html:43
#: templates/membership/membership_detail.html:45
#: templates/membership/membership_detail.html:32
#: templates/membership/membership_detail.html:35
msgid "Good"
msgstr "Gut"
#: templates/membership/membership_detail.html:45
#: templates/membership/membership_detail.html:35
msgid "Current Season"
msgstr "Aktuelle Saison"
#: templates/membership/membership_detail.html:55
#: templates/membership/membership_detail.html:42
msgid "private data"
msgstr "Private Daten"
#: templates/membership/membership_detail.html:43
msgid "This data can only be seen by yourself and members of the board."
msgstr ""
"Diese Angaben können nur von dir selbst und von Mitgliedern des Vorstandes "
"eingesehen werden."
#: templates/membership/membership_detail.html:45
msgid "name"
msgstr "Name"
#: templates/membership/membership_detail.html:50
msgid "Member Since"
msgstr "Mitglied seit"
#: templates/membership/membership_detail.html:51
msgid "Last Login"
msgstr "Letzte Anmeldung"
#: templates/membership/membership_detail.html:54
msgid "Edit Profile"
msgstr "Profil bearbeiten"
#: templates/membership/membership_detail.html:59
#: templates/membership/membership_detail.html:55
#: templates/registration/password_change_form.html:23
msgid "Change Password"
msgstr "Passwort ändern"
#: templates/membership/membership_detail.html:63
#: templates/membership/membership_detail.html:67
#: templates/membership/membership_detail.html:71
#, python-format
msgid "Associate with %(name)s"
msgstr "Verbinde mit %(name)s"
#: templates/membership/membership_form.html:4
#: templates/membership/membership_form.html:6
#: templates/membership/membership_form.html:11
@@ -357,35 +398,22 @@ msgid "Save"
msgstr "Speichern"
#: templates/membership/register_form.html:4
#: templates/membership/register_form.html:7
msgid "Registration"
msgstr "Registrieren"
#: templates/membership/register_form.html:9
msgid ""
"After you've provided your account data, you'll receive\n"
" an email asking you to verify your email address. You have to click on the\n"
" link in this verification email to confirm your email address, otherwise\n"
" your can't login."
msgstr ""
"Nach dem du deine Daten eingegeben hast, wirst du eine E-Mail zur Bestätigung bekommen.\n"
"Bitte klicke auf den Link in dieser E-Mail zur Verifizierung, erst dann ist die Anmeldung möglich."
#: templates/membership/register_form.html:10
msgid "Login crendentials"
msgstr "Anmeldedaten"
#: templates/membership/register_form.html:20
msgid "name"
msgstr "Name"
#: templates/membership/register_form.html:16
msgid "Club membership"
msgstr "Vereinsmitgliedschaft"
#: templates/membership/register_form.html:26
#: templates/registration/login.html:41
msgid "login"
msgstr "Anmelden"
#: templates/membership/register_form.html:39
#: templates/membership/register_form.html:27
msgid "reset"
msgstr "Zurücksetzen"
#: templates/membership/register_form.html:41
#: templates/registration/login.html:35
#: templates/membership/register_form.html:29
msgid "register"
msgstr "Registrieren"
@@ -395,93 +423,22 @@ msgstr "Registrieren"
msgid "Activation sent"
msgstr "Aktivierung wurde zugesendet"
#: templates/registration/login.html:4 templates/registration/login.html:11
#: templates/registration/login.html:53
#: templates/registration/password_reset_complete.html:13
msgid "Login"
#: templates/registration/login.html:17
msgid "login"
msgstr "Anmelden"
#: templates/registration/login.html:17
msgid "Have you already registered?"
msgstr "Bereits registriert?"
#: templates/registration/login.html:19
msgid "Your username and password didn't match. Please try again."
msgstr "Anmeldung fehlgeschlagen, bitte Benutzername und Passwort überprüfen."
#: templates/registration/login.html:18
#| msgid ""
#| "\n"
#| "<p>As a registered member you can:</p>\n"
#| "<ul>\n"
#| " <li>leave comments on this page.</li>\n"
#| " <li>subscribe to our Newsletter</li>\n"
#| " <li>apply to a membership to our club</li>\n"
#| " <li>club-members have access to our ranking-system</li>\n"
#| "</ul>\n"
msgid ""
"\n"
" <p>As a registered member you can:</p>\n"
" <ul>\n"
" <li>leave comments on this page.</li>\n"
" <li>subscribe to our Newsletter</li>\n"
" <li>apply to a membership to our club</li>\n"
" <li>club-members have access to our ranking-system</li>\n"
" </ul>\n"
" "
msgstr ""
"\n"
"<p>Als registrierter auf dieser Seite kannst du:</p>\n"
"<ul>\n"
" <li>Kommentare auf dieser Seite hinterlassen.</li>\n"
" <li>Dich für unseren Newsletter anmelden</li>\n"
" <li>Mitglied in unserem Verein werden</li>\n"
" <li>Vereinsmitglieder haben auch vollen Zugang zu unserem Ranking System</li>\n"
"</ul>"
#: templates/registration/login.html:27
#| msgid ""
#| "\n"
#| "<p>You can register here with your Google, or Facebook account.\n"
#| "If you don't own such an account, or do not want to use it for authentication,\n"
#| "you can fill out our registration form.</p>\n"
msgid ""
"\n"
" <p>You can register here with your Google, or Facebook account.\n"
" If you don't own such an account, or do not want to use it for\n"
" authentication,\n"
" you can fill out our registration form.</p>\n"
" "
msgstr ""
"\n"
"<p>Du kannst dich auch über deinen Facebook, Google, oder Twitter Account anmelden.\n"
"Wenn du so etwas nicht besitzt, oder nicht verwenden möchtest, \n"
"kannst du auch das Registrierungsformular ausfüllen.</p>"
#: templates/registration/login.html:45
#| msgid "Your username and password didn't match. Please try again."
msgid ""
"Your username and password didn't match. Please try\n"
" again."
msgstr ""
"Benutzername und Passwort stimmen nicht überein. Bitte noch einmal "
"versuchen."
#: templates/registration/login.html:50
#: templates/registration/login.html:22
msgid "Forgot your Password?"
msgstr "Passwort vergessen?"
#: templates/registration/login.html:60
msgid "or login with an existing Account"
msgstr "oder über einen existierenden Account anmelden"
#: templates/registration/login.html:63
msgid "Login with Facebook"
msgstr "Über Facebook anmelden"
#: templates/registration/login.html:66
msgid "Login with Twitter"
msgstr "Über Twitter anmelden"
#: templates/registration/login.html:69
msgid "Login with Google"
msgstr "Über Google Anmelden"
#: templates/registration/login.html:26
#: templates/registration/password_reset_complete.html:13
msgid "Login"
msgstr "Anmelden"
#: templates/registration/password_change_done.html:4
#: templates/registration/password_change_done.html:7
@@ -500,11 +457,11 @@ msgstr "Passwort wechseln"
#: templates/registration/password_change_form.html:10
msgid ""
"Please enter your old password, for security's sake, and then enter your new"
" password twice so we can verify you typed it in correctly."
"Please enter your old password, for security's sake, and then enter your new "
"password twice so we can verify you typed it in correctly."
msgstr ""
"Zur Sicherheit bitte altes Passwort einmal und das gewünschte neue Passwort"
" zweimal angeben, so können Tippfehler abgefangen werden."
"Zur Sicherheit bitte altes Passwort einmal und das gewünschte neue Passwort "
"zweimal angeben, so können Tippfehler abgefangen werden."
#: templates/registration/password_reset_complete.html:4
#: templates/registration/password_reset_complete.html:6
@@ -574,9 +531,83 @@ msgid "User Profile changed successfully"
msgstr "Benutzerprofil erfolgreich geändert."
#: views.py:112
#| msgid "No %(verbose_name)s found matching the query"
msgid "No Membership found matching the query"
msgstr "Keine Mitgliedschaft gefunden welche der Anfrage entspricht"
msgstr "Kein Mitglied gefunden welche der Anfrage entspricht"
#~ msgid "Associate with %(name)s"
#~ msgstr "Verbinde mit %(name)s"
#~ msgid "or"
#~ msgstr "oder"
#~ msgid "Login with Facebook"
#~ msgstr "Über Facebook anmelden"
#~ msgid "Login with Twitter"
#~ msgstr "Über Twitter anmelden"
#~ msgid "Login with Google"
#~ msgstr "Über Google Anmelden"
#~ msgid ""
#~ "After you've provided your account data, you'll receive\n"
#~ " an email asking you to verify your email address. You have to click "
#~ "on the\n"
#~ " link in this verification email to confirm your email address, "
#~ "otherwise\n"
#~ " your can't login."
#~ msgstr ""
#~ "Nach dem du deine Daten eingegeben hast, wirst du eine E-Mail zur "
#~ "Bestätigung bekommen.\n"
#~ "Bitte klicke auf den Link in dieser E-Mail zur Verifizierung, erst dann "
#~ "ist die Anmeldung möglich."
#~ msgid "Have you already registered?"
#~ msgstr "Bereits registriert?"
#~ msgid ""
#~ "\n"
#~ " <p>As a registered member you can:</p>\n"
#~ " <ul>\n"
#~ " <li>leave comments on this page.</li>\n"
#~ " <li>subscribe to our Newsletter</li>\n"
#~ " <li>apply to a membership to our club</li>\n"
#~ " <li>club-members have access to our ranking-system</li>\n"
#~ " </ul>\n"
#~ " "
#~ msgstr ""
#~ "\n"
#~ "<p>Als registrierter auf dieser Seite kannst du:</p>\n"
#~ "<ul>\n"
#~ " <li>Kommentare auf dieser Seite hinterlassen.</li>\n"
#~ " <li>Dich für unseren Newsletter anmelden</li>\n"
#~ " <li>Mitglied in unserem Verein werden</li>\n"
#~ " <li>Vereinsmitglieder haben auch vollen Zugang zu unserem Ranking "
#~ "System</li>\n"
#~ "</ul>"
#~ msgid ""
#~ "\n"
#~ " <p>You can register here with your Google, or Facebook account.\n"
#~ " If you don't own such an account, or do not want to use it for\n"
#~ " authentication,\n"
#~ " you can fill out our registration form.</p>\n"
#~ " "
#~ msgstr ""
#~ "\n"
#~ "<p>Du kannst dich auch über deinen Facebook, Google, oder Twitter Account "
#~ "anmelden.\n"
#~ "Wenn du so etwas nicht besitzt, oder nicht verwenden möchtest, \n"
#~ "kannst du auch das Registrierungsformular ausfüllen.</p>"
#~ msgid "or login with an existing Account"
#~ msgstr "oder über einen existierenden Account anmelden"
#~ msgid ""
#~ "For your membership, we need this. Please fill out this "
#~ "field yet."
#~ msgstr ""
#~ "Diese Angabe wird für eine Mitgliedschaft benötigt, bitte ausfüllen."
#~ msgid "Given Name"
#~ msgstr "Vorname"
@@ -585,9 +616,9 @@ msgstr "Keine Mitgliedschaft gefunden welche der Anfrage entspricht"
#~ msgstr "Nachname"
#~ msgid ""
#~ "The Username can only contain the letters from A to Z, Numbers, and "
#~ "the underscore. It must be at least 2 characters long, and cannot be"
#~ " longer the 30. The first character must be a letter."
#~ "The Username can only contain the letters from A to Z, Numbers, "
#~ "and the underscore. It must be at least 2 characters long, and "
#~ "cannot be longer the 30. The first character must be a letter."
#~ msgstr ""
#~ "Der Benutzername kann aus den Buchstaben A-Z, Ziffern und dem Unterstrich "
#~ "bestehen. Es sollte wenigstens 2, aber maximal 30 Zeichen lang sein. Das "

View File

@@ -1,17 +1,17 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.core.validators
import django.contrib.auth.models
from django.conf import settings
import django.core.validators
import django.utils.timezone
from django.conf import settings
from django.db import models, migrations
import membership.models
import utils
class Migration(migrations.Migration):
dependencies = [
('auth', '0006_require_contenttypes_0002'),
]
@@ -21,56 +21,94 @@ class Migration(migrations.Migration):
name='Membership',
fields=[
('id', models.AutoField(verbose_name='ID',
serialize=False, auto_created=True, primary_key=True)),
serialize=False, auto_created=True,
primary_key=True)),
('password', models.CharField(
max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(
null=True, verbose_name='last login', blank=True)),
('is_superuser', models.BooleanField(default=False,
help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, max_length=30, validators=[django.core.validators.RegexValidator(
'^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.', 'invalid')], help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', unique=True, verbose_name='username')),
help_text='Designates that this user has all permissions without explicitly assigning them.',
verbose_name='superuser status')),
('username', models.CharField(error_messages={
'unique': 'A user with that username already exists.'},
max_length=30, validators=[
django.core.validators.RegexValidator(
'^[\\w.@+-]+$',
'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.',
'invalid')],
help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.',
unique=True,
verbose_name='username')),
('first_name', models.CharField(max_length=30,
verbose_name='first name', blank=True)),
verbose_name='first name',
blank=True)),
('last_name', models.CharField(max_length=30,
verbose_name='last name', blank=True)),
verbose_name='last name',
blank=True)),
('email', models.EmailField(max_length=254,
verbose_name='email address', blank=True)),
verbose_name='email address',
blank=True)),
('is_staff', models.BooleanField(default=False,
help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
help_text='Designates whether the user can log into this admin site.',
verbose_name='staff status')),
('is_active', models.BooleanField(
default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
default=True,
help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.',
verbose_name='active')),
('date_joined', models.DateTimeField(
default=django.utils.timezone.now, verbose_name='date joined')),
('gender', models.CharField(max_length=1, verbose_name='Geschlecht', choices=[
(b'm', 'M\xe4nnlich'), (b'f', 'Weiblich')])),
default=django.utils.timezone.now,
verbose_name='date joined')),
('gender',
models.CharField(max_length=1, verbose_name='Geschlecht',
choices=[
(b'm', 'M\xe4nnlich'),
(b'f', 'Weiblich')])),
('website', models.URLField(blank=True)),
('avatar', models.ImageField(storage=utils.OverwriteStorage(
), null=True, upload_to=membership.models.get_upload_path, blank=True)),
), null=True, upload_to=membership.models.get_upload_path,
blank=True)),
('membership', models.BooleanField(default=False,
help_text='Ja, ich bin mit den Statuen einverstanden und m\xf6chte Mitglied werden.', verbose_name='Mitgliedschaft')),
help_text='Ja, ich bin mit den Statuen einverstanden und m\xf6chte Mitglied werden.',
verbose_name='Mitgliedschaft')),
('birthday', models.DateField(null=True,
verbose_name='Geburtstag', blank=True)),
verbose_name='Geburtstag',
blank=True)),
('telephone', models.CharField(max_length=30,
null=True, verbose_name='Telefon', blank=True)),
null=True,
verbose_name='Telefon',
blank=True)),
('street_name', models.CharField(max_length=75,
null=True, verbose_name='Adresse', blank=True)),
null=True,
verbose_name='Adresse',
blank=True)),
('post_code', models.PositiveSmallIntegerField(
null=True, verbose_name='Postleitzahl', blank=True)),
('city', models.CharField(max_length=75,
null=True, verbose_name='Ort', blank=True)),
null=True, verbose_name='Ort',
blank=True)),
('deposit', models.PositiveSmallIntegerField(
default=0, editable=False)),
('registration_date', models.DateField(auto_now_add=True)),
('paid_until', models.DateField(null=True,
verbose_name='Bezahlt bis', blank=True)),
verbose_name='Bezahlt bis',
blank=True)),
('confirmed', models.BooleanField(default=False,
help_text='Diese Person hat ihre Mitgliedschaft bezahlt', verbose_name='Best\xe4tigt')),
help_text='Diese Person hat ihre Mitgliedschaft bezahlt',
verbose_name='Best\xe4tigt')),
('comment', models.TextField(blank=True)),
('groups', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True,
help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', verbose_name='groups')),
('user_permissions', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Permission',
blank=True, help_text='Specific permissions for this user.', verbose_name='user permissions')),
('groups', models.ManyToManyField(related_query_name='user',
related_name='user_set',
to='auth.Group', blank=True,
help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.',
verbose_name='groups')),
('user_permissions',
models.ManyToManyField(related_query_name='user',
related_name='user_set',
to='auth.Permission',
blank=True,
help_text='Specific permissions for this user.',
verbose_name='user permissions')),
],
options={
'ordering': ('last_name', 'first_name'),
@@ -86,11 +124,13 @@ class Migration(migrations.Migration):
name='ActivationRequest',
fields=[
('id', models.AutoField(verbose_name='ID',
serialize=False, auto_created=True, primary_key=True)),
serialize=False, auto_created=True,
primary_key=True)),
('activation_key', models.CharField(
max_length=40, verbose_name='Aktivierungsschl\xfcssel')),
('user', models.OneToOneField(
verbose_name='Benutzer', to=settings.AUTH_USER_MODEL)),
verbose_name='Benutzer', to=settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)),
],
options={
'verbose_name': 'Ausstehende Aktivierung',

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.1.5 on 2019-01-06 18:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('membership', '0007_auto_20171115_0653'),
]
operations = [
migrations.AlterField(
model_name='membership',
name='last_name',
field=models.CharField(blank=True, max_length=150, verbose_name='last name'),
),
]

View File

@@ -7,8 +7,8 @@ from os import path
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.core.urlresolvers import reverse
from django.db import models
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import ugettext as _
from easy_thumbnails.fields import ThumbnailerImageField
@@ -80,6 +80,7 @@ class ActivationRequest(models.Model):
"""
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
verbose_name=_('user')
)
activation_key = models.CharField(_('activation key'), max_length=40)
@@ -149,7 +150,7 @@ class Membership(AbstractUser):
max_length=1,
choices=GENDER_CHOICES,
blank=True,
null=True
null=True,
)
website = models.URLField(blank=True)
avatar = ThumbnailerImageField(
@@ -205,8 +206,10 @@ class Membership(AbstractUser):
help_text=_('This person has paid the membership fee.')
)
# comment = models.TextField(blank=True)
# objects = MembershipManager()
nullable_personal_data = (
'gender', 'birthday', 'telephone', 'street_name', 'post_code', 'city')
blankable_personal_data = (
'email', 'password', 'first_name', 'last_name', 'website',)
class Meta(object):
"""To manage object ordering and dispalynames on the admin interface."""
@@ -215,6 +218,10 @@ class Membership(AbstractUser):
verbose_name = _('Membership')
verbose_name_plural = _('Memberships')
@property
def full_name(self):
return " ".join([self.last_name, self.first_name])
def __str__(self):
return self.username
@@ -226,8 +233,8 @@ class Membership(AbstractUser):
"""Save the Useraccount, and add him tho the "Paid Membership" group
if he activated the "membership" checkbox and has been validated.
:param args: passed through the save() method from django
:param kwargs: passed through the save() method from django
:param args: passed through the save() method from django
:param kwargs: passed through the save() method from django
"""
super(Membership, self).save(*args, **kwargs)
if self.confirmed:

View File

@@ -1,5 +1,4 @@
{% extends "base.html" %}
{% load i18n comments thumbnail %}
{% extends "base.html" %}{% load i18n comments thumbnail %}
{% block title %}{{ membership.first_name }} {{membership.last_name}}{% endblock %}
@@ -7,81 +6,61 @@
{% block navigation %}
<ul id="navigation">
<li><a href="{% url 'player-ladder-score' membership.username %}">{% trans "Ladder Hanchans" %}</a></li>
<li><a href="{% url 'player-kyu-score' membership.username %}">{% trans "Kyu Hanchans" %}</a></li>
<li><a href="{% url 'player-dan-score' membership.username %}">{% trans "Dan Hanchans" %}</a></li>
<li><a href="{% url 'player-invalid-score' membership.username %}">{% trans "Invalid Hanchans" %}</a></li>
<li><a href="{% url 'maistar-player-games' membership.username %}">{% trans "Mai-Star Games" %}</a></li>
</ul>
{% endblock %}
<li><a href="{% url 'player-ladder-score' membership.username %}">{% trans "Ladder Hanchans" %}</a></li>
<li><a href="{% url 'player-kyu-score' membership.username %}">{% trans "Kyu Hanchans" %}</a></li>
<li><a href="{% url 'player-dan-score' membership.username %}">{% trans "Dan Hanchans" %}</a></li>
<li><a href="{% url 'player-invalid-score' membership.username %}">{% trans "Invalid Hanchans" %}</a></li>
<li><a href="{% url 'maistar-player-games' membership.username %}">{% trans "Mai-Star Games" %}</a></li>
</ul>{% endblock %}
{% block maincontent %}
{% if membership.avatar %}
<img class="grid_3" src="{% thumbnail membership.avatar '220x220' crop='smart' %}" alt="{% trans 'Profile Image' %}"/>
{% else %}
<div class="grid_3"> Noch kein Foto hoch geladen</div>
{% endif %}
{% block maincontent %}{% if membership.avatar %}
<img class="grid_3" src="{% thumbnail membership.avatar '220x220' crop='smart' %}" alt="{% trans 'Profile Image' %}"/>{% else %}
<div class="grid_3"> Noch kein Foto hoch geladen</div>{% endif %}
<div class="grid_6">
<ul>
<li><strong>Name:</strong> {{membership.first_name}} {{membership.last_name}}</li>
<li><strong>{% trans "Member Since" %}:</strong> {{membership.date_joined}}</li>
<li><strong>{% trans "Last Login" %}:</strong> {{membership.last_login}}</li>
{% if website %}
<li><strong>Homepage:</strong> <a href="{{website}}">{{website}}</a></li>
{% endif %}
</ul>
{% if kyu_dan_ranking %}
<h3>Mahjong</h3>
<ul>
{% if kyu_dan_ranking.dan %}
<li><strong>{{kyu_dan_ranking.dan}}. Dan: </strong> {{ kyu_dan_ranking.dan_points }} {% trans 'Points' %}</li>
{% elif kyu_dan_ranking.kyu%}
<li><strong>{{kyu_dan_ranking.kyu}}. Kyu: </strong> {{ kyu_dan_ranking.kyu_points }} {% trans 'Points' %}</li>
{% endif %}
<li><strong>{% trans 'Games Total' %}: </strong> {{ kyu_dan_ranking.hanchan_count }} Hanchans -
{{kyu_dan_ranking.won_hanchans }} {% trans 'Won' %} / {{ kyu_dan_ranking.good_hanchans }} {% trans 'Good' %}
</li>
<li><strong>{% trans 'Current Season' %}: </strong> {{ ladder_ranking.hanchan_count }} Hanchans - {{ ladder_ranking.won_hanchans }} {% trans 'Won' %} / {{ ladder_ranking.good_hanchans }} {% trans 'Good' %}
</li>
</ul>
<div class="{% if membership == user or perms.membership.change_membership%}grid_4{% else %}grid_9{% endif %}" >
{% if kyu_dan_ranking %}
<h3>Mahjong</h3>
<ul>
{% if kyu_dan_ranking.dan %}
<li>
<strong>{{kyu_dan_ranking.dan}}. Dan: </strong> {{ kyu_dan_ranking.dan_points }} {% trans 'Points' %} ({% trans 'Maximum' %}: {{ kyu_dan_ranking.max_dan_points }})
</li>
{% elif kyu_dan_ranking.kyu%}
<li><strong>{{kyu_dan_ranking.kyu}}. Kyu: </strong> {{ kyu_dan_ranking.kyu_points }} {% trans 'Points' %}</li>
{% endif %}
<li>
<strong>{% trans 'Games Total' %}: </strong> {{ kyu_dan_ranking.hanchan_count }} Hanchans - {{kyu_dan_ranking.won_hanchans }} {% trans 'Won' %} / {{ kyu_dan_ranking.good_hanchans }} {% trans 'Good' %}
</li>
<li>
<strong>{% trans 'Current Season' %}: </strong> {{ ladder_ranking.hanchan_count }} Hanchans - {{ ladder_ranking.won_hanchans }} {% trans 'Won' %} / {{ ladder_ranking.good_hanchans }} {% trans 'Good' %}
</li>
</ul>
{% endif %}
</div>
{% if membership == user or perms.membership.change_membership%}
<div class="grid_5">
<h3>{% trans 'private data' %}</h3>
<p>{% blocktrans %}This data can only be seen by yourself and members of the board.{% endblocktrans %} </p>
<ul>
{% if membership.first_name %}<li><strong>{% trans "name" %}:</strong> {{membership.first_name}} {{membership.last_name}}</li>{% endif %}
{% if membership.birthday %}<li><strong>{% trans "birthday" %}:</strong> {{membership.birthday}}</li>{% endif %}
{% if membership.street_name %}<li><strong>{% trans "Address" %}:</strong> <address>{{ membership.street_name }}<br/> {{ membership.post_code }} {{ membership.city }}</address></li>{% endif %}
{% if membership.email %}<li><strong>{% trans "Email" %}:</strong> <a href="mailto:{{ membership.email }}">{{membership.email}}</a></li>{% endif %}
{% if membership.telephone %}<li><strong>{% trans "Telephone" %}:</strong> <a href="tel:{{ membership.telephone }}">{{membership.telephone}}</a></li>{% endif %}
<li><strong>{% trans "Member Since" %}:</strong> {{membership.date_joined}}</li>
<li><strong>{% trans "Last Login" %}:</strong> {{membership.last_login}}</li>
</ul>
{% ifequal membership user %}
<a href="{% url 'membership-edit' membership.username %}" class="button"> <span class="fa fa-pencil"></span> {% trans "Edit Profile" %} </a>
<a href="{% url 'password_change' %}" class="button"> <span class="fa fa-key"></span> {% trans 'Change Password' %}</a>
{% endifequal %}
</div>
{% endif %}
{% ifequal membership user %}
<div class="grid_3">
<a href="{% url 'membership-edit' membership.username %}" class="button">
<span class="fa fa-pencil"></span>
{% trans "Edit Profile" %}
</a>
<a href="{% url 'password_change' %}" class="button">
<span class="fa fa-key"></span>
{% trans 'Change Password' %}
</a>
<a class="button" href="{% url 'social:begin' 'facebook' %}" rel="nofollow">
<span class="fa fa-facebook"></span>
{% blocktrans with 'Facebook' as name %}Associate with {{ name }}{% endblocktrans %}
</a>
<a class="button" href="{% url 'social:begin' 'twitter' %}" rel="nofollow">
<span class="fa fa-twitter"></span>
{% blocktrans with 'Twitter' as name %}Associate with {{ name }}{% endblocktrans %}
</a>
<a class="button" href="{% url 'social:begin' 'google-oauth2' %}" rel="nofollow">
<span class="fa fa-google-plus"></span>
{% blocktrans with 'Google' as name %}Associate with {{ name }}{% endblocktrans %}
</a>
</div>
{% endifequal %}
{% block score_list %} {% endblock %}
{% endblock %}
{% block score_list %} {% endblock %}{% endblock %}
{% block comments %}
{% if membership %}
{% render_comment_list for membership %}
{% render_comment_form for membership %}
{% endif %}
{% endblock %}
{% block comments %}{% if membership %}{% render_comment_list for membership %}{% render_comment_form for membership %}{% endif %}{% endblock %}
{% block buttonbar%}{%endblock%}

View File

@@ -17,18 +17,22 @@
</p>
</fieldset>
</form>
{% endblock %}
{% block javascript %}
$(function() {
$( "#id_birthday" ).datepicker({
changeMonth: true,
changeYear: true,
dateFormat: 'yy-mm-dd',
yearRange: '-50,0',
firstDay: 1
});
});
<script type="text/javascript">
function togglePersonalData(event) {
var membership = document.getElementById("id_membership").checked;
var input_elements = ["id_first_name", "id_last_name", "id_birthday"];
for (var i = 0; i < input_elements.length; i++) {
element_id = input_elements[i]
element = document.getElementById(element_id);
element.disabled = !membership;
if (membership == 0) {element.value = ""};
};
};
document.addEventListener('DOMContentLoaded',function() {document.querySelector('#id_membership').onchange=togglePersonalData;},false);
togglePersonalData();
</script>
{% endblock %}
{% block buttonbar %}{% endblock %}

View File

@@ -3,37 +3,25 @@
{% block title %}{% trans "Registration"%}{% endblock %}
{% block teaser%}
<h1>{% trans "Registration"%}</h1>
<div id="teaser_text">
{% blocktrans %}After you've provided your account data, you'll receive
an email asking you to verify your email address. You have to click on the
link in this verification email to confirm your email address, otherwise
your can't login.{% endblocktrans %}
</div>
{% endblock %}
{% block maincontent %}
<form method="post" action="{% url 'membership-register' %}">
{% csrf_token %}
<fieldset class="grid_5">
<legend>{% trans "name"%}</legend>
{% get_fieldset "gender, first_name, last_name, username" from form as form1 %}
{% with form1 as form %}{% include "form.html" %}{% endwith %}
</fieldset>
<fieldset class="grid_7">
<legend>{% trans "login"%}</legend>
{% get_fieldset "email, password1, password2, recaptcha" from form as form1 %}
<legend>{% trans "Login crendentials"%}</legend>
{% get_fieldset "username, email, password1, password2, privacy_policy, recaptcha" from form as form1 %}
{% with form1 as form %}{% include "form.html" %}{% endwith %}
</fieldset>
<fieldset class="grid_12">
<legend>{% trans "Membership"%}</legend>
{% get_fieldset "membership, birthday, street_name, post_code, city, telephone, website" from form as form2 %}
<fieldset class="grid_5">
<legend>{% trans "Club membership"%}</legend>
{% get_fieldset "membership, first_name, last_name, birthday" from form as form2 %}
{% with form2 as form %}{% include "form.html" %}{% endwith %}
</fieldset>
<div class="grid_12">
{{ current_page.content }}
</div>
<div class="grid_12">
<p class="buttonbar">
<button type="reset"><span class="fa fa-undo"></span> {% trans 'reset' %}
@@ -42,4 +30,19 @@
</p>
</div>
</form>
{% endblock %}
<script type="text/javascript">
function togglePersonalData(event) {
var membership = document.getElementById("id_membership").checked;
var input_elements = ["id_first_name", "id_last_name", "id_birthday"];
for (var i = 0; i < input_elements.length; i++) {
element_id = input_elements[i]
element = document.getElementById(element_id);
element.disabled = !membership;
if (membership == 0) {element.value = ""};
};
};
document.addEventListener('DOMContentLoaded',function() {document.querySelector('#id_membership').onchange=togglePersonalData;},false);
togglePersonalData();
</script>
{% endblock %}

View File

@@ -0,0 +1,6 @@
{% extends "base.html" %}{% load i18n %}
{% block content %}
{{ current_page.content }}
{% endblock %}

View File

@@ -1,72 +1,32 @@
{% extends "base.html" %}
{% load i18n %}
{% extends "base.html" %}{% load i18n %}
{% block title %}{% trans 'Login' %}{% endblock %}
{% block description %}Anmelden auf Kasu.at{% endblock %}
{% block extra_head %}
<link rel="canonical" href="{% url 'login' %}"/>
{% endblock %}
<link rel="canonical" href="{% url 'login' %}"/>{% endblock %}
{% block teaser %}<h1>{% trans 'Login' %}</h1>{% endblock %}
{% block maincontent %}
<form method="post" action="{% url 'login' %}">
<h2 class="grid_12">Auf der Seite Anmelden</h2>
<div class="grid_7">
<h2>{% trans "Have you already registered?" %}</h2>
{% blocktrans %}
<p>As a registered member you can:</p>
<ul>
<li>leave comments on this page.</li>
<li>subscribe to our Newsletter</li>
<li>apply to a membership to our club</li>
<li>club-members have access to our ranking-system</li>
</ul>
{% endblocktrans %}
{% blocktrans %}
<p>You can register here with your Google, or Facebook account.
If you don't own such an account, or do not want to use it for
authentication,
you can fill out our registration form.</p>
{% endblocktrans %}
<p class="buttonbar">
<a href="{% url 'membership-register' %}" class="button"><span
class="fa fa-user-plus"></span> {%trans "register"%}</a></p>
</div>
{% csrf_token %}
<fieldset class="grid_5">
<legend>{% trans 'login' %}</legend>
{% csrf_token %}
{% include 'form.html' %}
{% if form.errors %}
<p>{% blocktrans %}Your username and password didn't match. Please try
again.{% endblocktrans %}</p>
{% endif %}
<input type="hidden" name="next" value="{{next}}"/>
<p><a href="{% url 'password_reset' %}">
{% trans 'Forgot your Password?'%}</a></p>
<div class="buttonbar">
<button type="submit"><span class="fa fa-sign-in"></span>
{% trans 'Login' %}
</button>
</div>
</fieldset>
<div class="grid_5">
<h2>{% trans "or login with an existing Account" %}</h2>
<a rel="nofollow" href="{% url 'social:begin' 'facebook' %}"><span
class="fa fa-facebook fa-5x"
title="{% trans 'Login with Facebook' %}"></span></a>
<a rel="nofollow" href="{% url 'social:begin' 'twitter' %}"><span
class="fa fa-twitter fa-5x"
title="{% trans 'Login with Twitter' %}"></span></a>
<a rel="nofollow" href="{% url 'social:begin' 'google-oauth2' %}"><span
class="fa fa-google-plus fa-5x"
title="{% trans 'Login with Google' %}"></span></a>
<div class="grid_6">
{{ current_page.content }}
</div>
<div class="grid_6">
<fieldset>
<legend>{% trans 'login' %}</legend>
{% csrf_token %} {% include 'form.html' %} {% if form.errors %}
<p>{% blocktrans %}Your username and password didn't match. Please try again.{% endblocktrans %}</p>
{% endif %} <input type="hidden" name="next" value="{{next}}"/>
<p>
<a href="{% url 'password_reset' %}"> {% trans 'Forgot your Password?'%}</a>
</p>
<div class="buttonbar">
<button type="submit">
<span class="fa fa-sign-in"></span> {% trans 'Login' %}
</button>
</div>
</fieldset>
</div>
</form>
{% endblock %}

View File

@@ -9,6 +9,7 @@ from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.MembershipDetail.as_view()),
url(r'^activate/(?P<activation_key>[\d\w]+)/$',
views.ActivateRegistration.as_view(),
name='membership-activate-registration'),

View File

@@ -7,7 +7,7 @@ from django import http
from django.conf import settings
from django.contrib import auth, messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.urlresolvers import reverse
from django.urls import reverse
from django.http import Http404
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _
@@ -106,7 +106,7 @@ class MembershipDetail(LoginRequiredMixin, generic.DetailView):
try:
if self.kwargs.get('username'):
return queryset.get(username=self.kwargs['username'])
elif self.request.user.is_authenticated():
else:
return self.request.user
except models.Membership.DoesNotExist:
raise Http404(_("No Membership found matching the query"))
@@ -114,7 +114,7 @@ class MembershipDetail(LoginRequiredMixin, generic.DetailView):
def get_context_data(self, **kwargs):
""" Add the ladder ranking and the Kyu and Dan ranking the user profile.
:return: array with the context data
:return: array with the context data
"""
context = generic.DetailView.get_context_data(self, **kwargs)
try:
@@ -139,11 +139,11 @@ class RegisterForm(generic.FormView):
@method_decorator(csp_update(**RECAPTCHA_CSP))
def dispatch(self, request, *args, **kwargs):
"""Overwrite to add googles recaptcha ressoruces to the
"""Overwrite to add googles recaptcha ressoruces to the
Content-Security_Policity HTTP Headers.
:param request:
:param args:
:param kwargs:
:param request:
:param args:
:param kwargs:
:return: """
return super(RegisterForm, self).dispatch(request, *args, **kwargs)

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: kasu.utils\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-04-27 09:49+0200\n"
"POT-Creation-Date: 2019-12-13 23:38+0100\n"
"PO-Revision-Date: 2016-09-28 00:24+0200\n"
"Last-Translator: Christian Berg <xeniac@posteo.at>\n"
"Language-Team: Kasu <verein@kasu.at>\n"

View File

@@ -67,7 +67,7 @@ class MassMailer(object):
def set_header(self, name, value):
"""Add or modify an E-Mail Header to the Messages
:param name: The Header Name that should be added
:param value: THe Header Value that shoud be added or set
"""

View File

@@ -3,31 +3,56 @@ between Tags, an at the beginning and the end of the content."""
from django.utils.html import strip_spaces_between_tags
class CompressHtmlMiddleware(object):
"""This Middleware compresses strips the spaces between tags, and at the
class CompressHtmlMiddleware:
"""This Middleware compresses strips the spaces between tags, and at the
beginning and the end of the content."""
# TODO: Port to django 1.10 and upward
def __init__(self, get_response):
"""
:param get_response:
:param get_response:
"""
self.get_response = get_response
regex = ">[\s]*<"
def __call__(self, request):
"""
:param request:
:return:
:param request:
:return:
"""
# Code to be executed for each request before
# the view (and later middleware) are called.
response = self.get_response(request)
if 'text/html' in response['Content-Type']:
response.content = strip_spaces_between_tags(
response.content).strip()
return response
class SetRemoteAddrFromForwardedFor:
def __init__(self, get_response):
self.get_response = get_response
# One-time configuration and initialization.
def __call__(self, request):
# Code to be executed for each request before
# the view (and later middleware) are called.
response = self.get_response(request)
# Code to be executed for each request/response after
# the view is called.
return response
def process_request(self, request):
try:
real_ip = request.META['HTTP_X_FORWARDED_FOR']
real_ip = real_ip.split(",")[0]
except KeyError:
pass
else:
# HTTP_X_FORWARDED_FOR can be a comma-separated list of IPs.
# Take just the first one.
request.META['REMOTE_ADDR'] = real_ip

View File

@@ -44,7 +44,7 @@ class HtmlTestCase(unittest.TestCase):
xxx-xxxx</FONT></DIV></BODY></HTML>
''',
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">'
)
)
)
def test_html_cleaner(self):