From d62f549a30621be4a603f4425b1069a131bb29f6 Mon Sep 17 00:00:00 2001 From: Xeniac Date: Thu, 23 Nov 2017 22:26:22 +0100 Subject: [PATCH] Squashed commit of the following: commit bb5081a78b947e1764ed585beca67c009a6bd66b Author: Xeniac 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 854fd387408f84384c346c9bf8f911e0954718d8 Author: Xeniac 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 6de1ecb1026105ad9c58b8a348a30070541063d1 Author: Christian Berg Date: Thu Nov 23 14:15:36 2017 +0100 add a latest method to query the latest x events commit bf12060c3bc981a4e951fbef75044d5f26977140 Author: Christian Berg Date: Thu Nov 23 14:15:12 2017 +0100 add a latest method to query the latest x events commit 5ad628f33a60f592314e9bc65e2ba25b39905a12 Author: Christian Berg Date: Mon Nov 20 07:47:47 2017 +0100 Changed PlayerDanScore to only list non-legacy hanchans commit 36272c60d62d1addd7a8043f84f82cafffed7499 Author: Christian Berg Date: Mon Nov 20 07:42:44 2017 +0100 fixed import of MIN_HANCHANS_FOR_LADDER that moved to settings commit c428f6ed1f653478beca17c3dd474d41dd1d04d1 Author: Christian Berg Date: Mon Nov 20 07:41:04 2017 +0100 Updated docstrings for new since and until kwargs commit 9276e97c36e08376e919886d87430f2bb8aefa91 Author: Christian Berg 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 fd244f10e841d1015982ee402311695192af6af8 Author: Christian Berg 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 0a45cf1fd85602e2b5b9ac8467860adf5d40f8ae Author: Christian Berg 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. --- .../migrations/0006_auto_20171115_0653.py | 91 +++++++++++++++ src/events/managers.py | 4 + .../migrations/0008_auto_20171115_0653.py | 20 ++++ src/events/mixins.py | 16 ++- src/events/views.py | 3 +- src/kasu/settings.py | 1 + .../management/commands/export_ranking.py | 88 +++++++++++--- .../management/commands/resetdanranking.py | 32 +++++ .../management/commands/update_ranking.py | 34 +++--- src/mahjong_ranking/managers.py | 110 ++++++++++++++---- src/mahjong_ranking/middleware.py | 14 +-- .../migrations/0005_auto_20171115_0653.py | 29 +++++ src/mahjong_ranking/mixins.py | 18 +++ src/mahjong_ranking/models.py | 93 +++++++++------ src/mahjong_ranking/tests.py | 8 +- src/mahjong_ranking/views.py | 104 ++++++----------- .../migrations/0006_auto_20171115_0653.py | 19 +++ .../migrations/0007_auto_20171115_0653.py | 26 +++++ 18 files changed, 530 insertions(+), 180 deletions(-) create mode 100644 src/content/migrations/0006_auto_20171115_0653.py create mode 100644 src/events/migrations/0008_auto_20171115_0653.py create mode 100644 src/mahjong_ranking/management/commands/resetdanranking.py create mode 100644 src/mahjong_ranking/migrations/0005_auto_20171115_0653.py create mode 100644 src/mahjong_ranking/mixins.py create mode 100644 src/maistar_ranking/migrations/0006_auto_20171115_0653.py create mode 100644 src/membership/migrations/0007_auto_20171115_0653.py diff --git a/src/content/migrations/0006_auto_20171115_0653.py b/src/content/migrations/0006_auto_20171115_0653.py new file mode 100644 index 0000000..648dc72 --- /dev/null +++ b/src/content/migrations/0006_auto_20171115_0653.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.7 on 2017-11-15 05:53 +from __future__ import unicode_literals + +import ckeditor_uploader.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('content', '0005_auto_20161012_2236'), + ] + + operations = [ + migrations.AlterField( + model_name='article', + name='content_de', + field=ckeditor_uploader.fields.RichTextUploadingField(verbose_name='Inhalt'), + ), + migrations.AlterField( + model_name='article', + name='content_en', + field=ckeditor_uploader.fields.RichTextUploadingField(blank=True, verbose_name='Content'), + ), + migrations.AlterField( + model_name='article', + name='headline_en', + field=models.CharField(blank=True, max_length=255, verbose_name='Headline'), + ), + migrations.AlterField( + model_name='article', + name='image', + field=models.ImageField(blank=True, null=True, upload_to='news/', verbose_name='Bild'), + ), + migrations.AlterField( + model_name='article', + name='slug', + field=models.SlugField(unique_for_month='date_created', verbose_name='Slug'), + ), + migrations.AlterField( + model_name='category', + name='image', + field=models.ImageField(blank=True, null=True, upload_to='news/categories/', verbose_name='Bild'), + ), + migrations.AlterField( + model_name='page', + name='content_de', + field=ckeditor_uploader.fields.RichTextUploadingField(blank=True, verbose_name='Inhalt'), + ), + migrations.AlterField( + model_name='page', + name='content_en', + field=ckeditor_uploader.fields.RichTextUploadingField(blank=True, verbose_name='Content'), + ), + migrations.AlterField( + model_name='page', + name='menu_name_de', + field=models.CharField(help_text='Ein kurzer Name für den Menüeintrag', max_length=255, verbose_name='Menü Name'), + ), + migrations.AlterField( + model_name='page', + name='menu_name_en', + field=models.CharField(blank=True, help_text='Ein kurzer Name für den Menüeintrag', max_length=255, verbose_name='Menu Name'), + ), + migrations.AlterField( + model_name='page', + name='pdf_de', + field=models.FileField(blank=True, null=True, upload_to='pdf/de/'), + ), + migrations.AlterField( + model_name='page', + name='pdf_en', + field=models.FileField(blank=True, null=True, upload_to='pdf/en/'), + ), + migrations.AlterField( + model_name='page', + name='template', + field=models.CharField(default='content/page.html', max_length=255, verbose_name='Vorlage'), + ), + migrations.AlterField( + model_name='page', + name='title_de', + field=models.CharField(help_text="The page title as you'd like it to be seen by the public", max_length=255, verbose_name='Titel'), + ), + migrations.AlterField( + model_name='page', + name='title_en', + field=models.CharField(blank=True, help_text="The page title as you'd like it to be seen by the public", max_length=255, verbose_name='Title'), + ), + ] diff --git a/src/events/managers.py b/src/events/managers.py index d845727..ceeda5a 100644 --- a/src/events/managers.py +++ b/src/events/managers.py @@ -31,6 +31,10 @@ class EventManager(models.Manager): """Returns all past events.""" return self.filter(start__lt=now()) + def latest(self, limit=None): + result = self.filter(start__lt=now()).order_by('-start', '-end') + return result[0:limit] if limit else result + def upcoming(self, limit=None): """Returns the next 'limit' upcoming events. diff --git a/src/events/migrations/0008_auto_20171115_0653.py b/src/events/migrations/0008_auto_20171115_0653.py new file mode 100644 index 0000000..61fc91a --- /dev/null +++ b/src/events/migrations/0008_auto_20171115_0653.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.7 on 2017-11-15 05:53 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('events', '0007_auto_20161012_2224'), + ] + + operations = [ + migrations.AlterField( + model_name='location', + name='country', + field=models.CharField(choices=[('GB', 'Vereinigtes Königreich'), ('AF', 'Afghanistan'), ('AX', 'Aland Islands'), ('AL', 'Albanien'), ('DZ', 'Algerien'), ('AS', 'Amerikanisch-Samoa'), ('AD', 'Andorra'), ('AO', 'Angola'), ('AI', 'Anguilla'), ('AQ', 'Antarktika'), ('AG', 'Antigua und Barbuda'), ('AR', 'Argentinien'), ('AM', 'Armenien'), ('AW', 'Aruba'), ('AU', 'Australien'), ('AT', 'Österreich'), ('AZ', 'Aserbaidschan'), ('BS', 'Bahamas'), ('BH', 'Bahrein'), ('BD', 'Bangladesch'), ('BB', 'Barbados'), ('BY', 'Weißrussland'), ('BE', 'Belgien'), ('BZ', 'Belize'), ('BJ', 'Benin'), ('BM', 'Bermuda'), ('BT', 'Bhutan'), ('BO', 'Bolivien'), ('BA', 'Bosnien und Herzegowina'), ('BW', 'Botswana'), ('BV', 'Bouvet Island'), ('BR', 'Brasilien'), ('IO', 'British Indian Ocean Territory'), ('BN', 'Brunei Darussalam'), ('BG', 'Bulgarien'), ('BF', 'Burkina Faso'), ('BI', 'Burundi'), ('KH', 'Kambodscha'), ('CM', 'Kamerun'), ('CA', 'Kanada'), ('CV', 'Cape Verde'), ('KY', 'Cayman Islands'), ('CF', 'Zentralafrikanische Republik'), ('TD', 'Tschad'), ('CL', 'Chile'), ('CN', 'China'), ('CX', 'Christmas Island'), ('CC', 'Cocos (Keeling) Islands'), ('CO', 'Kolumbien'), ('KM', 'Komoren'), ('CG', 'Kongo'), ('CD', 'Kongo, Demokratische Republik'), ('CK', 'Cook-Inseln'), ('CR', 'Costa Rica'), ('CI', "Cote d'Ivoire"), ('HR', 'Kroatien'), ('CU', 'Kuba'), ('CY', 'Zypern'), ('CZ', 'Tschechische Republik'), ('DK', 'Dänemark'), ('DJ', 'Dschibuti'), ('DM', 'Dominica'), ('DO', 'Dominikanische Republik'), ('EC', 'Ecuador'), ('EG', 'Ägypten'), ('SV', 'El Salvador'), ('GQ', 'Äquatorial-Guinea'), ('ER', 'Eritrea'), ('EE', 'Estland'), ('ET', 'Äthiopien'), ('FK', 'Falklandinseln (Malvinas)'), ('FO', 'Färöer-Inseln'), ('FJ', 'Fidschi'), ('FI', 'Finnland'), ('FR', 'Frankreich'), ('GF', 'Französisch-Guayana'), ('PF', 'Französisch-Polynesien'), ('TF', 'Französisch Südliche Territorien'), ('GA', 'Gabun'), ('GM', 'Gambia'), ('GE', 'Georgia'), ('DE', 'Deutschland'), ('GH', 'Ghana'), ('GI', 'Gibraltar'), ('GR', 'Griechenland'), ('GL', 'Grönland'), ('GD', 'Grenada'), ('GP', 'Guadeloupe'), ('GU', 'Guam'), ('GT', 'Guatemala'), ('GG', 'Guernsey'), ('GN', 'Guinea'), ('GW', 'Guinea-Bissau'), ('GY', 'Guyana'), ('HT', 'Haiti'), ('HM', 'Heard und McDonald Inseln'), ('VA', 'Heiliger Stuhl (Vatikanstadt)'), ('HN', 'Honduras'), ('HK', 'Hongkong'), ('HU', 'Ungarn'), ('IS', 'Island'), ('IN', 'Indien'), ('ID', 'Indonesien'), ('IR', 'Iran, Islamische Republik'), ('IQ', 'Irak'), ('IE', 'Irland'), ('IM', 'Isle of Man'), ('IL', 'Israel'), ('IT', 'Italien'), ('JM', 'Jamaika'), ('JP', 'Japan'), ('JE', 'Jersey'), ('JO', 'Jordan'), ('KZ', 'Kasachstan'), ('KE', 'Kenia'), ('KI', 'Kiribati'), ('KP', 'Korea, Demokratische Volksrepublik'), ('KR', 'Korea, Republik'), ('KW', 'Kuwait'), ('KG', 'Kirgisistan'), ('LA', 'Lao Demokratischen Volksrepublik'), ('LV', 'Lettland'), ('LB', 'Libanon'), ('LS', 'Lesotho'), ('LR', 'Liberia'), ('LY', 'Libyen'), ('LI', 'Liechtenstein'), ('LT', 'Litauen'), ('LU', 'Luxemburg'), ('MO', 'Macao'), ('MK', 'Mazedonien, die ehemalige jugoslawische Republik'), ('MG', 'Madagaskar'), ('MW', 'Malawi'), ('MY', 'Malaysia'), ('MV', 'Malediven'), ('ML', 'Mali'), ('MT', 'Malta'), ('MH', 'Marshall Islands'), ('MQ', 'Martinique'), ('MR', 'Mauretanien'), ('MU', 'Mauritius'), ('YT', 'Mayotte'), ('MX', 'Mexiko'), ('FM', 'Mikronesien, Föderierte Staaten von'), ('MD', 'Moldawien'), ('MC', 'Monaco'), ('MN', 'Mongolei'), ('ME', 'Montenegro'), ('MS', 'Montserrat'), ('MA', 'Marokko'), ('MZ', 'Mosambik'), ('MM', 'Myanmar'), ('NA', 'Namibia'), ('NR', 'Nauru'), ('NP', 'Nepal'), ('NL', 'Niederlande'), ('AN', 'Niederländische Antillen'), ('NC', 'Neukaledonien'), ('NZ', 'New Zealand'), ('NI', 'Nicaragua'), ('NE', 'Niger'), ('NG', 'Nigeria'), ('NU', 'Niue'), ('NF', 'Norfolk Island'), ('MP', 'Northern Mariana Islands'), ('NO', 'Norwegen'), ('OM', 'Oman'), ('PK', 'Pakistan'), ('PW', 'Palau'), ('PS', 'Palästinensische Autonomiegebiete'), ('PA', 'Panama'), ('PG', 'Papua-Neuguinea'), ('PY', 'Paraguay'), ('PE', 'Peru'), ('PH', 'Philippinen'), ('PN', 'Pitcairn'), ('PL', 'Polen'), ('PT', 'Portugal'), ('PR', 'Puerto Rico'), ('QA', 'Katar'), ('RE', 'Wiedervereinigung'), ('RO', 'Rumänien'), ('RU', 'Russischen Föderation'), ('RW', 'Ruanda'), ('BL', 'Saint Barthelemy'), ('SH', 'Saint Helena'), ('KN', 'Saint Kitts und Nevis'), ('LC', 'Santa Lucia'), ('MF', 'Santa Martin'), ('PM', 'Saint Pierre und Miquelon'), ('VC', 'Saint Vincent und die Grenadinen'), ('WS', 'Samoa'), ('SM', 'San Marino'), ('ST', 'Sao Tome und Principe'), ('SA', 'Saudi-Arabien'), ('SN', 'Senegal'), ('RS', 'Serbien'), ('SC', 'Seychellen'), ('SL', 'Sierra Leone'), ('SG', 'Singapur'), ('SK', 'Slowakei'), ('SI', 'Slowenien'), ('SB', 'Salomon-Inseln'), ('SO', 'Somalia'), ('ZA', 'Südafrika'), ('GS', 'Südgeorgien und die Südlichen Sandwichinseln'), ('ES', 'Spanien'), ('LK', 'Sri Lanka'), ('SD', 'Sudan'), ('SR', 'Suriname'), ('SJ', 'Svalbard und Jan Mayen'), ('SZ', 'Swaziland'), ('SE', 'Schweden'), ('CH', 'Schweiz'), ('SY', 'Arabische Republik Syrien'), ('TW', 'Taiwan, Province of China'), ('TJ', 'Tadschikistan'), ('TZ', 'Tansania, Vereinigte Republik'), ('TH', 'Thailand'), ('TL', 'Timor-Leste'), ('TG', 'Togo'), ('TK', 'Tokelau'), ('TO', 'Tonga'), ('TT', 'Trinidad und Tobago'), ('TN', 'Tunesien'), ('TR', 'Türkei'), ('TM', 'Turkmenistan'), ('TC', 'Turks-und Caicosinseln'), ('TV', 'Tuvalu'), ('UG', 'Uganda'), ('UA', 'Ukraine'), ('AE', 'Vereinigte Arabische Emirate'), ('US', 'Vereinigte Staaten'), ('UM', 'United States Minor Outlying Islands'), ('UY', 'Uruguay'), ('UZ', 'Usbekistan'), ('VU', 'Vanuatu'), ('VE', 'Venezuela'), ('VN', 'Vietnam'), ('VG', 'Virgin Islands, British'), ('VI', 'Virgin Islands, US'), ('WF', 'Wallis und Futuna'), ('EH', 'Westsahara'), ('YE', 'Jemen'), ('ZM', 'Sambia'), ('ZW', 'Zimbabwe')], max_length=2, verbose_name='Land'), + ), + ] diff --git a/src/events/mixins.py b/src/events/mixins.py index cb8f372..9b9f915 100644 --- a/src/events/mixins.py +++ b/src/events/mixins.py @@ -1,4 +1,6 @@ """Mixins for Events.""" +from django.http import Http404 + from . import models @@ -9,7 +11,6 @@ class EventArchiveMixin(object): date_field = 'start' make_object_list = True model = models.Event - ordering = ('start', 'end') paginate_by = 15 template_name = 'events/event_archive.html' @@ -40,3 +41,16 @@ class EventDetailMixin(object): elif hasattr(self, 'object') and hasattr(self.object, 'event'): context['event'] = self.object.event return context + + def get_queryset(self): + """set event attribute from the URL kwarg event and + load all related objects from the set model. + + :return: a django QuerySets + """ + try: + self.event = models.Event.objects.get(pk=self.kwargs['event']) + queryset = self.model.objects.filter(event=self.event) + except models.Event.DoesNotExist: + raise Http404(_('Event does not exist')) + return queryset.prefetch_related() diff --git a/src/events/views.py b/src/events/views.py index 1dfa8a0..e92bd3a 100644 --- a/src/events/views.py +++ b/src/events/views.py @@ -30,7 +30,6 @@ class DeleteEventPhoto(PermissionRequiredMixin, mixins.EventDetailMixin, class EventArchiveIndex(mixins.EventArchiveMixin, generic.ArchiveIndexView): """Index of the event archive, displays the upcoming events first.""" allow_empty = True - ordering = ('-start', '-end') class EventArchiveMonth(mixins.EventArchiveMixin, generic.MonthArchiveView): @@ -73,7 +72,7 @@ class EventForm(PermissionRequiredMixin, mixins.EventDetailMixin, if self.kwargs.get('pk') else models.Event() -class EventGallery(mixins.EventDetailMixin, generic.ListView): +class EventGallery(generic.ListView): """Display a overview of all event photo albums.""" template_name = 'events/photo_gallery.html' queryset = models.Event.objects.filter( diff --git a/src/kasu/settings.py b/src/kasu/settings.py index a7a9009..ea63e90 100644 --- a/src/kasu/settings.py +++ b/src/kasu/settings.py @@ -273,6 +273,7 @@ KYU_RANKS = ( 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) diff --git a/src/mahjong_ranking/management/commands/export_ranking.py b/src/mahjong_ranking/management/commands/export_ranking.py index 164ced4..247f5a2 100644 --- a/src/mahjong_ranking/management/commands/export_ranking.py +++ b/src/mahjong_ranking/management/commands/export_ranking.py @@ -1,11 +1,14 @@ """Export Mahjong Rankings as excel files.""" -from datetime import date -from operator import itemgetter +import os +from datetime import date, time, datetime import openpyxl +from django.conf import settings from django.core.management.base import BaseCommand -from openpyxl.styles import Border +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 @@ -34,7 +37,24 @@ 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. @@ -45,8 +65,9 @@ def geneate_excel(): 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: - print(sheet) workbook.remove(sheet) return workbook @@ -55,6 +76,8 @@ 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 @@ -90,10 +113,10 @@ def generate_sheet(workbook, title, columns_settings, json_data): ws.column_dimensions[settings['col']].width = settings['width'] -def export_season_rankings(workbook): - json_data = sorted(SeasonRanking.objects.json_data(), - key=itemgetter('placement')) - title = "Mahjong Ladder - {}".format(date.today().year) +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}, @@ -101,7 +124,7 @@ def export_season_rankings(workbook): 'style': 'content', 'width': 25}, {'col': 'C', 'title': '⌀ Platz', 'attr': 'avg_placement', - 'style': 'int', 'width': 8}, + 'style': 'float', 'width': 8}, {'col': 'D', 'title': '⌀ Punkte', 'attr': 'avg_score', 'style': 'float', 'width': 12}, {'col': 'E', 'title': 'Hanchans', 'attr': 'hanchan_count', @@ -111,7 +134,6 @@ def export_season_rankings(workbook): {'col': 'G', 'title': 'Gewonnen', 'attr': 'won_hanchans', 'style': 'int', 'width': 10}, ) - generate_sheet( workbook=workbook, title=title, @@ -119,7 +141,8 @@ def export_season_rankings(workbook): json_data=json_data) -def export_kyu_dan_rankings(workbook): +def export_kyu_dan_rankings(workbook, until): + KyuDanRanking.objects.update(until=until) json_data = KyuDanRanking.objects.json_data() title = "Kyū & Dan Rankings" columns_settings = ( @@ -136,7 +159,9 @@ def export_kyu_dan_rankings(workbook): {'col': 'F', 'title': 'Gut', 'attr': 'good_hanchans', 'style': 'int', 'width': 5}, {'col': 'G', 'title': 'Gewonnen', 'attr': 'won_hanchans', - 'style': 'int', 'width': 10}, + 'style': 'int', 'width': 8}, + {'col': 'H', 'title': 'letzte Hanchan', 'attr': 'last_hanchan_date', + 'style': 'date', 'width': 16}, ) generate_sheet( workbook=workbook, @@ -147,12 +172,43 @@ def export_kyu_dan_rankings(workbook): 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) - export_kyu_dan_rankings(workbook) - workbook.save('sample.x') - workbook.save('mahjong_rankings_{}.xlsx'.format(str(date.today()))) + 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() + diff --git a/src/mahjong_ranking/management/commands/resetdanranking.py b/src/mahjong_ranking/management/commands/resetdanranking.py new file mode 100644 index 0000000..2ead5e5 --- /dev/null +++ b/src/mahjong_ranking/management/commands/resetdanranking.py @@ -0,0 +1,32 @@ +""" +Rest all dan points to 0 at a given date. +""" + +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.utils import timezone + +class Command(BaseCommand): + """ Recalculate all Kyu/Dan Rankings """ + + help = "reset every dan player to 1st dan with 0 points." + + def add_arguments(self, parser): + 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: + ranking.dan = 1 + ranking.dan_points = 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 + ranking.save() + + diff --git a/src/mahjong_ranking/management/commands/update_ranking.py b/src/mahjong_ranking/management/commands/update_ranking.py index 692a8e1..d7a8fd7 100644 --- a/src/mahjong_ranking/management/commands/update_ranking.py +++ b/src/mahjong_ranking/management/commands/update_ranking.py @@ -5,27 +5,29 @@ Recalculate Mahjong Rankings... """ from django.core.management.base import BaseCommand - -from mahjong_ranking import LOGGER +from datetime import date, datetime, 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 """ help = "Recalculate all 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') + def handle(self, *args, **options): - old_attr = {'dan': None, 'dan_points': None, - 'kyu': None, 'kyu_points': None, 'won_hanchans': None, - 'good_hanchans': None, 'hanchan_count': None} - for ranking in models.KyuDanRanking.objects.all(): - old_attr = {attr: getattr(ranking, attr) for attr in old_attr.keys()} - ranking.recalculate() - for attr, old_value in old_attr.items(): - if getattr(ranking, attr) != old_value: - LOGGER.warning( - "%(user)s recalc shows differences in %(attr)s! old: %(old)d, new: %(new)d", - {'user': ranking.user, 'attr': attr, - 'old': old_value, 'new': getattr(ranking, attr)} - ) + since = options.get('since', None) + until = options.get('until', None) + force_recalc = options.get('forecerecalc', False) + 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) \ No newline at end of file diff --git a/src/mahjong_ranking/managers.py b/src/mahjong_ranking/managers.py index aa0421a..674f734 100644 --- a/src/mahjong_ranking/managers.py +++ b/src/mahjong_ranking/managers.py @@ -1,7 +1,8 @@ """ObjectManagers for the Django Models used in the Mahjong-Ranking.""" from datetime import date - +from . import LOGGER from django.db import models +from django.conf import settings class HanchanManager(models.Manager): @@ -12,23 +13,31 @@ class HanchanManager(models.Manager): """ use_for_related_fields = True - def confirmed_hanchans(self, user=None, **filter_args): + 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 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 :param filter_args: To add specific arguments to the Django filter. :return: QuerySet Object """ if user: - return self.user_hanchans(user, confirmed=True, **filter_args) - else: - return self.filter(confirmed=True, **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) + if until: + hanchans = hanchans.filter(start__lte=until) + return hanchans - def dan_hanchans(self, user, **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. :param user: Only return Hanchans where this user participated. + :param since: only return Hanchans played since the given datetime :param filter_args: To add specific arguments to the Django filter. :return: QuerySet Object """ @@ -38,15 +47,18 @@ class HanchanManager(models.Manager): models.Q(player3=user, player3_dan_points__isnull=False) | models.Q(player4=user, player4_dan_points__isnull=False) ).filter(confirmed=True, **filter_args) + if since: + queryset = queryset.filter(start__gt=since) queryset = queryset.select_related().order_by('-start') - [ hanchan.get_playerdata(user) for hanchan in queryset ] + [hanchan.get_playerdata(user) for hanchan in queryset] return queryset - def kyu_hanchans(self, user, **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. :param user: Only return Hanchans where this user participated. + :param since: only return Hanchans played since the given datetime :param filter_args: To add specific arguments to the Django filter. :return: QuerySet Object """ @@ -56,25 +68,30 @@ class HanchanManager(models.Manager): models.Q(player3=user, player3_kyu_points__isnull=False) | models.Q(player4=user, player4_kyu_points__isnull=False) ).filter(confirmed=True, **filter_args) + if since: + queryset = queryset.filter(start__gt=since) queryset = queryset.select_related().order_by('-start') - [ hanchan.get_playerdata(user) for hanchan in queryset ] + [hanchan.get_playerdata(user) for hanchan in queryset] return queryset - def season_hanchans(self, user=None, season=None): + def season_hanchans(self, user=None, season=None, until=None): """Return all Hanchans that belong to a given or the current season. :param user: Only return Hanchans where this user participated. :param season: the year of the wanted season, current year if None. :return: QuerySet Object """ - season = season or date.today().year - return self.confirmed_hanchans(user=user, season=season) + try: + season = season or until.year + except AttributeError: + season = date.today().year + return self.confirmed(user=user, season=season, until=until) - def user_hanchans(self, user, since=None, **filter_args): + def user_hanchans(self, user, since=None, until=None, **filter_args): """Return all Hanchans where a specific user has participated. :param user: Return Hanchans where this user participated. - :param since: optional a date value since when you want to hanchans + :param since: only return Hanchans played since the given datetime :param filter_args: To add specific arguments to the Django filter. :return: a QuerySet Object """ @@ -82,15 +99,16 @@ class HanchanManager(models.Manager): models.Q(player1=user) | models.Q(player2=user) | models.Q(player3=user) | models.Q(player4=user) ) + queryset = queryset.filter(**filter_args) if since: - queryset = queryset.filter(start__gte=since, **filter_args) - else: - queryset = queryset.filter(**filter_args) + queryset = queryset.filter(start__gte=since) + if until: + queryset = queryset.filter(start__lte=until) queryset = queryset.select_related().order_by('-start') - [ hanchan.get_playerdata(user) for hanchan in queryset ] + [hanchan.get_playerdata(user) for hanchan in queryset] return queryset - def unconfirmed_hanchans(self, user=None, **filter_args): + def unconfirmed(self, user=None, **filter_args): """ Return all Hanchans that have been set to unconfirmed. :param user: Only return Hanchans where this user participated. @@ -158,8 +176,27 @@ class SeasonRankingManager(models.Manager): }) return json_data -class KyuDanRankingManager(models.Manager): + def update(self, season=None, until=None, force_recalc=False): + try: + season = season or until.year + except AttributeError: + season = date.today().year + if until or force_recalc: + for ranking in self.filter(season=season): + ranking.recalculate(until=until) + for placement, ranking in enumerate(self.season_rankings(season), start=1): + ranking.placement = placement + ranking.save(force_update=True, update_fields=['placement']) + def season_rankings(self, season=None): + season = season or date.today().year + rankings = self.filter( + season=season, + hanchan_count__gt=settings.MIN_HANCHANS_FOR_LADDER) + return rankings.order_by('avg_placement', '-avg_score') + + +class KyuDanRankingManager(models.Manager): def json_data(self): """ Get all Rankings for a given Season and return them as a list of dict objects, suitable for JSON exports and other processings. @@ -172,9 +209,12 @@ class KyuDanRankingManager(models.Manager): values = values.values('user_id', 'user__username', 'user__first_name', 'user__last_name', 'dan', 'dan_points', 'kyu', 'kyu_points', - 'hanchan_count', 'won_hanchans', 'good_hanchans') + 'hanchan_count', 'won_hanchans', 'good_hanchans', + 'last_hanchan_date') for user in values: - if user['dan']: + if user['hanchan_count'] == 0: + continue + elif user['dan']: rank = '{}. Dan'.format(user['dan']) points = user['dan_points'] else: @@ -183,11 +223,31 @@ class KyuDanRankingManager(models.Manager): json_data.append({ 'user_id': user['user_id'], 'username': user['user__username'], - 'full_name': " ".join([user['user__last_name'], user['user__first_name']]), + 'full_name': " ".join( + [user['user__last_name'], user['user__first_name']]), 'rank': rank, 'points': points, 'hanchan_count': user['hanchan_count'], 'good_hanchans': user['good_hanchans'], - 'won_hanchans': user['won_hanchans'] + 'won_hanchans': user['won_hanchans'], + 'last_hanchan_date': user['last_hanchan_date'] + }) return json_data + + def update(self, since=None, until=None, force_recalc=False): + old_attr = {'dan': None, 'dan_points': None, + 'kyu': None, 'kyu_points': None, 'won_hanchans': None, + 'good_hanchans': None, 'hanchan_count': None} + for ranking in self.all(): + old_attr = {attr: getattr(ranking, attr) for attr in + old_attr.keys()} + ranking.calculate(since=since, until=until, + force_recalc=force_recalc) + for attr, old_value in old_attr.items(): + if getattr(ranking, attr) != old_value: + LOGGER.warning( + "%(user)s recalc shows differences in %(attr)s! old: %(old)d, new: %(new)d", + {'user': ranking.user, 'attr': attr, + 'old': old_value or 0, 'new': getattr(ranking, attr) or 0} + ) diff --git a/src/mahjong_ranking/middleware.py b/src/mahjong_ranking/middleware.py index 299edc1..2b71256 100644 --- a/src/mahjong_ranking/middleware.py +++ b/src/mahjong_ranking/middleware.py @@ -1,8 +1,7 @@ """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, MIN_HANCHANS_FOR_LADDER +from . import LOGGER class DenormalizationUpdateMiddleware(object): # Ignore PyLintBear (R0903) @@ -35,7 +34,7 @@ class DenormalizationUpdateMiddleware(object): # Ignore PyLintBear (R0903) user_id, hanchan_start = kyu_dan_ranking_queue.pop() kyu_dan_ranking = models.KyuDanRanking.objects.get_or_create( user_id=user_id)[0] - kyu_dan_ranking.recalculate(hanchan_start) + kyu_dan_ranking.calculate(since=hanchan_start) cache.set('kyu_dan_ranking_queue', set(), 360) ladder_ranking_queue = cache.get('ladder_ranking_queue', set()) @@ -58,12 +57,5 @@ class DenormalizationUpdateMiddleware(object): # Ignore PyLintBear (R0903) for season in season_queue: LOGGER.info(u'Recalculate placements for Season %d', season) - season_rankings = models.SeasonRanking.objects.filter( - season=season, hanchan_count__gt=MIN_HANCHANS_FOR_LADDER - ).order_by('avg_placement', '-avg_score') - placement = 1 - for ranking in season_rankings: - ranking.placement = placement - ranking.save(force_update=True) - placement += 1 + models.SeasonRanking.objects.update(season=season) return response diff --git a/src/mahjong_ranking/migrations/0005_auto_20171115_0653.py b/src/mahjong_ranking/migrations/0005_auto_20171115_0653.py new file mode 100644 index 0000000..50b21a8 --- /dev/null +++ b/src/mahjong_ranking/migrations/0005_auto_20171115_0653.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.7 on 2017-11-15 05:53 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mahjong_ranking', '0004_auto_20170218_1947'), + ] + + operations = [ + migrations.AlterModelOptions( + name='kyudanranking', + options={'ordering': ('-dan_points', 'dan', '-kyu_points'), 'verbose_name': 'Kyū/Dan Wertung', 'verbose_name_plural': 'Kyū/Dan Wertungen'}, + ), + migrations.AddField( + model_name='kyudanranking', + name='last_hanchan_date', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='kyudanranking', + name='wins_in_a_row', + field=models.PositiveIntegerField(default=0), + ), + ] diff --git a/src/mahjong_ranking/mixins.py b/src/mahjong_ranking/mixins.py new file mode 100644 index 0000000..f621d8a --- /dev/null +++ b/src/mahjong_ranking/mixins.py @@ -0,0 +1,18 @@ +from datetime import date +from .models import Hanchan, SeasonRanking +from events.models import Event + + +class MahjongMixin(object): + def get_context_data(self, **kwargs): + context = super(MahjongMixin, self).get_context_data(**kwargs) + try: + context['season'] = self.season + context['season_start'] = date(year=self.season, month=1, day=1) + context['season_end'] = date(year=self.season, month=12, day=31) + context['season_list'] = SeasonRanking.objects.season_list + except AttributeError: + pass + context['latest_hanchan_list'] = Hanchan.objects.confirmed()[:3] + context['latest_event_list'] = Event.objects.latest(limit=3) + return context diff --git a/src/mahjong_ranking/models.py b/src/mahjong_ranking/models.py index 76c252f..8ea23f2 100644 --- a/src/mahjong_ranking/models.py +++ b/src/mahjong_ranking/models.py @@ -5,6 +5,8 @@ from __future__ import division +from datetime import datetime, time + from django.conf import settings from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse @@ -56,7 +58,7 @@ class EventRanking(models.Model): ) sum_placement = 0.0 sum_score = 0.0 - event_hanchans = Hanchan.objects.confirmed_hanchans( + event_hanchans = Hanchan.objects.confirmed( user=self.user_id, event=self.event_id ) @@ -345,10 +347,10 @@ class KyuDanRanking(models.Model): legacy_hanchan_count = models.PositiveIntegerField(default=0) legacy_dan_points = models.PositiveIntegerField(default=0) legacy_kyu_points = models.PositiveIntegerField(default=0) - wins_in_a_row = 0 + wins_in_a_row = models.PositiveIntegerField(default=0) + last_hanchan_date = models.DateTimeField(blank=True, null=True) objects = managers.KyuDanRankingManager() - class Meta(object): ordering = ('-dan_points', 'dan', '-kyu_points') verbose_name = _(u'Kyū/Dan Ranking') @@ -436,36 +438,48 @@ class KyuDanRanking(models.Model): else: return reverse('player-kyu-score', args=[self.user.username]) - def recalculate(self, hanchan_start=None): + def calculate(self, since=None, until=None, force_recalc=False): """ Fetches all valid Hanchans from this Player and recalculates his Kyu/Dan Ranking. """ - self.dan = None - self.dan_points = self.legacy_dan_points or 0 - self.kyu = None - 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() - + valid_hanchans = Hanchan.objects.confirmed(user=self.user) + valid_hanchans = valid_hanchans.order_by('start') + if since and self.last_hanchan_date and since < self.last_hanchan_date: + force_recalc = True + if until and self.last_hanchan_date and until < self.last_hanchan_date: + force_recalc = True + if force_recalc: + # Setze alles auf die legacy Werte und berechne alles von neuem. + self.dan = None + self.dan_points = self.legacy_dan_points or 0 + self.kyu = None + 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.last_hanchan_date = None + if self.legacy_date: + since = timezone.make_aware( + datetime.combine(self.legacy_date, time(0, 0, 0))) + else: + since = None + elif self.last_hanchan_date: + since = self.last_hanchan_date + elif self.legacy_date: + since = timezone.make_aware( + datetime.combine(self.legacy_date, time(0, 0, 0)) + ) LOGGER.info( - "recalculating Kyu/Dan points for %s since %s...", - self.user, str(hanchan_start) + "recalculating Kyu/Dan points for %(user)s since %(since)s...", + {'user': self.user, 'since': str(since)} ) - valid_hanchans = Hanchan.objects.confirmed_hanchans( - user=self.user).order_by('start') - if self.legacy_date: - valid_hanchans = valid_hanchans.filter(start__date__gte=self.legacy_date) - - """ TODO: Hanchan Punkte nur neu berechnen wenn sie nach hachan_start - lagen. Es müssen aber alle durch die Schleife rennen, damit die Punkte - richtig gezählt werden.""" self.hanchan_count += valid_hanchans.count() for hanchan in valid_hanchans: hanchan.get_playerdata(self.user) - if hanchan_start and hanchan_start < hanchan.start: + if since and hanchan.start < since: + print(hanchan, "<", since, "no recalc") self.dan_points += hanchan.dan_points or 0 self.kyu_points += hanchan.kyu_points or 0 self.update_rank() @@ -480,20 +494,22 @@ class KyuDanRanking(models.Model): 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 - 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 + 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.save(force_update=True) + def update_hanchan_points(self, hanchan): """ Berechne die Kyu bzw. Dan Punkte für eine Hanchan neu. @@ -547,6 +563,7 @@ class KyuDanRanking(models.Model): 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): @@ -593,9 +610,9 @@ class SeasonRanking(models.Model): def get_absolute_url(self): return reverse('player-ladder-score', args=[self.user.username]) - def recalculate(self): + def recalculate(self, until=None): season_hanchans = Hanchan.objects.season_hanchans( - user=self.user, season=self.season) + user=self.user, season=self.season, until=until) sum_placement = 0 sum_score = 0 self.placement = None diff --git a/src/mahjong_ranking/tests.py b/src/mahjong_ranking/tests.py index 63f1a86..ca85a6c 100644 --- a/src/mahjong_ranking/tests.py +++ b/src/mahjong_ranking/tests.py @@ -34,7 +34,7 @@ class KyuDanTest(TestCase): for ranking in KyuDanRanking.objects.all(): original = {a: getattr(ranking, a) for a in self.equal_attrs} - ranking.recalculate() + ranking.calculate() for attr in self.equal_attrs: self.assertEqual( original[attr], @@ -54,7 +54,7 @@ class KyuDanTest(TestCase): for ranking in KyuDanRanking.objects.all(): original = {a: getattr(ranking, a) for a in self.equal_attrs} - confirmed_hanchans = Hanchan.objects.confirmed_hanchans( + confirmed_hanchans = Hanchan.objects.confirmed( user=ranking.user, since=ranking.legacy_date ) @@ -62,7 +62,7 @@ class KyuDanTest(TestCase): continue rnd = random.randrange(confirmed_hanchans.count()) since = confirmed_hanchans[rnd].start - ranking.recalculate(hanchan_start=since) + ranking.calculate(since=since) for attr in self.equal_attrs: self.assertEqual( original[attr], @@ -86,7 +86,7 @@ class KyuDanTest(TestCase): 'dan_points': ranking.legacy_dan_points or 0, 'kyu_points': ranking.legacy_kyu_points or 0 } - confirmed_hanchans = Hanchan.objects.confirmed_hanchans( + confirmed_hanchans = Hanchan.objects.confirmed( user=ranking.user, since=ranking.legacy_date ) diff --git a/src/mahjong_ranking/views.py b/src/mahjong_ranking/views.py index 44c2dcd..5394070 100644 --- a/src/mahjong_ranking/views.py +++ b/src/mahjong_ranking/views.py @@ -11,11 +11,11 @@ from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ from django.views import generic -from events.models import Event from events.mixins import EventDetailMixin from . import forms, models +from .mixins import MahjongMixin -kyu_dan_order = { +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'), '+hanchan_count': ('hanchan_count',), @@ -31,16 +31,17 @@ kyu_dan_order = { class DeleteHanchan(EventDetailMixin, PermissionRequiredMixin, generic.DeleteView): - """ - Fragt zuerst nach, ob die Hanchan wirklich gelöscht werden soll. - Wir die Frage mit "Ja" beantwortet, wird die die Hanchan gelöscht. - """ + """Deletes a Hanchan if confimration has been answerd with 'yes'.""" form_class = forms.HanchanForm model = models.Hanchan permission_required = 'mahjong_ranking.delete_hanchan' pk_url_kwarg = 'hanchan' def get_success_url(self): + """ + Return to the HachanList of the event form the deleted hanchan. + :return: URL of the EventHanchanList for the event + """ return reverse('event-hanchan-list', kwargs={'event': self.object.event.pk}) @@ -48,32 +49,30 @@ class DeleteHanchan(EventDetailMixin, PermissionRequiredMixin, class HanchanForm(SuccessMessageMixin, EventDetailMixin, PermissionRequiredMixin, generic.UpdateView): """ - Ein Formular um neue Hanchans anzulegen, bzw. eine bestehende zu - bearbeitsen + A Form to add a new or edit an existing Hanchan. """ form_class = forms.HanchanForm model = models.Hanchan permission_required = 'mahjong_ranking.add_hanchan' - def get_context_data(self, **kwargs): - context = generic.UpdateView.get_context_data(self, **kwargs) - context['event'] = self.event - return context - def get_form_class(self): """ - Users with edit Persmission will see the AdminForm to confirm - unconfirmed Hanchans. + Users with hanchan edit persmission can also un-/confirm hanchans. + :return: forms.HanchanForm, or forms.HanchanAdminForm """ - if self.request.user.has_perm('mahjong_ranking.change_hanchan'): - return forms.HanchanAdminForm - else: - return forms.HanchanForm + return forms.HanchanAdminForm if self.request.user.has_perm( + 'mahjong_ranking.change_hanchan') else forms.HanchanForm def get_object(self, queryset=None): + """ + load the hanchan form the db, or create a new one with the event set. + Also sets the event attribute. + :param queryset: + :return: models.Hanchan object + """ if self.kwargs.get('hanchan') and self.request.user.has_perm( 'mahjong_ranking.change_hanchan'): - hanchan = models.Hanchan.objects.get(id=self.kwargs['hanchan']) + hanchan = self.model.objects.get(id=self.kwargs['hanchan']) self.event = hanchan.event elif self.kwargs.get('event'): self.event = models.Event.objects.get(id=self.kwargs['event']) @@ -94,6 +93,11 @@ class HanchanForm(SuccessMessageMixin, EventDetailMixin, return reverse('add-hanchan-form', kwargs={'event': self.event.pk}) def get_success_message(self, cleaned_data): + """ + Get the right sucsess message for the django notification subsystem. + :param cleaned_data: + :return: Sucsess message + """ if self.kwargs.get('hanchan'): return _('%s has been updated successfully.') % self.object else: @@ -103,72 +107,36 @@ class HanchanForm(SuccessMessageMixin, EventDetailMixin, class EventHanchanList(EventDetailMixin, generic.ListView): - """ - Auflistung aller Hanchan die während der Veranstaltung gespielt wurden. - """ + "List all hanchans played on a given event." model = models.Hanchan template_name = 'mahjong_ranking/eventhanchan_list.html' - def get_queryset(self): - try: - self.event = models.Event.objects.get(pk=self.kwargs['event']) - queryset = models.Hanchan.objects.filter(event=self.event) - queryset = queryset.order_by('start') - return queryset - except models.Event.DoesNotExist: - raise django.http.Http404(_('Event does not exist')) - class EventRankingList(EventDetailMixin, generic.ListView): - """ - Anzeige des Eventrankings, daß erstellt wurde falls der Termin als internes - Turnier markiert wurde. - """ + """Display the event ranking for the given event.""" model = models.EventRanking - def get_queryset(self): - try: - self.event = models.Event.objects.get(pk=self.kwargs['event']) - queryset = models.EventRanking.objects.filter(event=self.event) - return queryset.prefetch_related() - except models.Event.DoesNotExist: - raise django.http.Http404(_('Event does not exist')) -class MahjongMixin(object): - def get_context_data(self, **kwargs): - context = super(MahjongMixin, self).get_context_data(**kwargs) - try: - context['season'] = self.season - context['season_start'] = date(year=self.season, month=1, day=1) - context['season_end'] = date(year=self.season, month=12, day=31) - context['season_list'] = models.SeasonRanking.objects.season_list - except AttributeError: - pass - context[ - 'latest_hanchan_list'] = \ - models.Hanchan.objects.confirmed_hanchans()[ - :3] - context['latest_event_list'] = Event.objects.upcoming(limit=3) - return context class KyuDanRankingList(MahjongMixin, generic.ListView): - """ - Anzeige aller Spiele mit ihrem Kyu bzw Dan Grad. - """ + """List all Players with an Kyu or Dan score. """ default_order = '-score' order_by = '' paginate_by = 25 def dispatch(self, request, *args, **kwargs): - self.order_by = kyu_dan_order[ + """Set the order_by settings, revert to default_order if necessary.""" + self.order_by = KYU_DAN_ORDER[ kwargs.get('order_by', self.default_order) ] - return generic.ListView.dispatch(self, request, *args, **kwargs) + return super(KyuDanRankingList, self).dispatch(request, *args, **kwargs) + def get_queryset(self): - queryset = models.KyuDanRanking.objects.all().order_by(*self.order_by) + queryset = models.KyuDanRanking.objects.filter( + hanchan_count__gt=0).order_by(*self.order_by) return queryset.select_related() @@ -223,14 +191,16 @@ class PlayerDanScore(PlayerScore): template_name = 'mahjong_ranking/player_dan_score.html' def get_queryset(self): - return models.Hanchan.objects.dan_hanchans(user=self.user) + 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) class PlayerInvalidScore(PlayerScore): template_name = 'mahjong_ranking/player_invalid_score.html' def get_queryset(self): - return models.Hanchan.objects.unconfirmed_hanchans(user=self.user) + return models.Hanchan.objects.unconfirmed(user=self.user) class PlayerKyuScore(PlayerScore): diff --git a/src/maistar_ranking/migrations/0006_auto_20171115_0653.py b/src/maistar_ranking/migrations/0006_auto_20171115_0653.py new file mode 100644 index 0000000..a9c742f --- /dev/null +++ b/src/maistar_ranking/migrations/0006_auto_20171115_0653.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.7 on 2017-11-15 05:53 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('maistar_ranking', '0005_auto_20170218_1947'), + ] + + operations = [ + migrations.AlterModelOptions( + name='game', + options={'ordering': ('-event__start', '-id')}, + ), + ] diff --git a/src/membership/migrations/0007_auto_20171115_0653.py b/src/membership/migrations/0007_auto_20171115_0653.py new file mode 100644 index 0000000..4d61c7d --- /dev/null +++ b/src/membership/migrations/0007_auto_20171115_0653.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.7 on 2017-11-15 05:53 +from __future__ import unicode_literals + +import django.contrib.auth.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('membership', '0006_auto_20160916_1759'), + ] + + operations = [ + migrations.AlterField( + model_name='membership', + name='gender', + field=models.CharField(blank=True, choices=[('m', 'Male'), ('f', 'Female')], max_length=1, null=True, verbose_name='Geschlecht'), + ), + migrations.AlterField( + model_name='membership', + name='username', + field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username'), + ), + ]