From 10c27784eedb66a09444b196e70f52c462765a28 Mon Sep 17 00:00:00 2001 From: Xeniac Date: Tue, 26 Dec 2017 21:45:39 +0100 Subject: [PATCH] =?UTF-8?q?XLSX=20Export=20vereinheitlicht.=20Spieler=20Ha?= =?UTF-8?q?nchanlisten=20k=C3=B6nnen=20nun=20als=20XLSX=20exportiert=20wer?= =?UTF-8?q?den.=20Anpassungen=20in=20den=20Einstellungen=20f=C3=BCr=20die?= =?UTF-8?q?=20parametisierten=20Kyu/Dan=20Berechnung.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../migrations/0007_auto_20171214_1215.py | 27 +++ .../migrations/0009_auto_20171214_1215.py | 32 +++ src/kasu/settings.py | 2 + .../templates/django/forms/widgets/date.html | 4 + .../django/forms/widgets/datetime.html | 4 + .../django/forms/widgets/html5input.html | 1 + .../templates/django/forms/widgets/time.html | 4 + src/kasu/urls.py | 2 +- src/kasu/xlsx.py | 147 ++++++++++++ .../management/commands/export_ranking.py | 214 ------------------ .../management/commands/exportranking.py | 126 +++++++++++ .../{update_ranking.py => updateranking.py} | 27 ++- src/mahjong_ranking/models.py | 14 +- .../mahjong_ranking/player_dan_score.html | 4 + .../mahjong_ranking/player_invalid_score.html | 10 +- .../mahjong_ranking/player_kyu_score.html | 10 +- .../mahjong_ranking/player_ladder_score.html | 5 +- src/mahjong_ranking/views.py | 214 +++++++++++------- .../migrations/0007_auto_20171214_1215.py | 52 +++++ src/membership/models.py | 4 + .../membership/membership_detail.html | 5 +- 22 files changed, 589 insertions(+), 320 deletions(-) create mode 100644 src/content/migrations/0007_auto_20171214_1215.py create mode 100644 src/events/migrations/0009_auto_20171214_1215.py create mode 100644 src/kasu/templates/django/forms/widgets/date.html create mode 100644 src/kasu/templates/django/forms/widgets/datetime.html create mode 100644 src/kasu/templates/django/forms/widgets/html5input.html create mode 100644 src/kasu/templates/django/forms/widgets/time.html create mode 100644 src/kasu/xlsx.py delete mode 100644 src/mahjong_ranking/management/commands/export_ranking.py create mode 100644 src/mahjong_ranking/management/commands/exportranking.py rename src/mahjong_ranking/management/commands/{update_ranking.py => updateranking.py} (54%) create mode 100644 src/maistar_ranking/migrations/0007_auto_20171214_1215.py diff --git a/.gitignore b/.gitignore index 7041e31..d8443f0 100644 --- a/.gitignore +++ b/.gitignore @@ -62,6 +62,7 @@ docs/_build/ target/ #Django Development +backup/ /bower_components/ /media/ /node_modules/ diff --git a/src/content/migrations/0007_auto_20171214_1215.py b/src/content/migrations/0007_auto_20171214_1215.py new file mode 100644 index 0000000..61398c5 --- /dev/null +++ b/src/content/migrations/0007_auto_20171214_1215.py @@ -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'), + ), + ] diff --git a/src/events/migrations/0009_auto_20171214_1215.py b/src/events/migrations/0009_auto_20171214_1215.py new file mode 100644 index 0000000..22dc786 --- /dev/null +++ b/src/events/migrations/0009_auto_20171214_1215.py @@ -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), + ), + ] diff --git a/src/kasu/settings.py b/src/kasu/settings.py index 111e0f9..e38a68c 100644 --- a/src/kasu/settings.py +++ b/src/kasu/settings.py @@ -259,7 +259,9 @@ 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), diff --git a/src/kasu/templates/django/forms/widgets/date.html b/src/kasu/templates/django/forms/widgets/date.html new file mode 100644 index 0000000..08c783d --- /dev/null +++ b/src/kasu/templates/django/forms/widgets/date.html @@ -0,0 +1,4 @@ +{% with type="date" %} +{% include "django/forms/widgets/html5input.html" %} +{% endwith %} + diff --git a/src/kasu/templates/django/forms/widgets/datetime.html b/src/kasu/templates/django/forms/widgets/datetime.html new file mode 100644 index 0000000..e3f044b --- /dev/null +++ b/src/kasu/templates/django/forms/widgets/datetime.html @@ -0,0 +1,4 @@ +{% with type="datetime-local" %} +{% include "django/forms/widgets/html5input.html" %} +{% endwith %} + diff --git a/src/kasu/templates/django/forms/widgets/html5input.html b/src/kasu/templates/django/forms/widgets/html5input.html new file mode 100644 index 0000000..f23b0ad --- /dev/null +++ b/src/kasu/templates/django/forms/widgets/html5input.html @@ -0,0 +1 @@ + diff --git a/src/kasu/templates/django/forms/widgets/time.html b/src/kasu/templates/django/forms/widgets/time.html new file mode 100644 index 0000000..c85e335 --- /dev/null +++ b/src/kasu/templates/django/forms/widgets/time.html @@ -0,0 +1,4 @@ +{% with type="time" %} +{% include "django/forms/widgets/html5input.html" %} +{% endwith %} + diff --git a/src/kasu/urls.py b/src/kasu/urls.py index 1af6c8a..05be4a4 100644 --- a/src/kasu/urls.py +++ b/src/kasu/urls.py @@ -32,7 +32,7 @@ urlpatterns = [ # Ignore PyLintBear (C0103) url(r'^add_page/(?P[\+\.\-\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[\+\.\-\d\w\/]+)/$', diff --git a/src/kasu/xlsx.py b/src/kasu/xlsx.py new file mode 100644 index 0000000..4a26a88 --- /dev/null +++ b/src/kasu/xlsx.py @@ -0,0 +1,147 @@ +""" +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) diff --git a/src/mahjong_ranking/management/commands/export_ranking.py b/src/mahjong_ranking/management/commands/export_ranking.py deleted file mode 100644 index 247f5a2..0000000 --- a/src/mahjong_ranking/management/commands/export_ranking.py +++ /dev/null @@ -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() - diff --git a/src/mahjong_ranking/management/commands/exportranking.py b/src/mahjong_ranking/management/commands/exportranking.py new file mode 100644 index 0000000..d7440a3 --- /dev/null +++ b/src/mahjong_ranking/management/commands/exportranking.py @@ -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) + 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() diff --git a/src/mahjong_ranking/management/commands/update_ranking.py b/src/mahjong_ranking/management/commands/updateranking.py similarity index 54% rename from src/mahjong_ranking/management/commands/update_ranking.py rename to src/mahjong_ranking/management/commands/updateranking.py index 4d6c1c5..546c78f 100644 --- a/src/mahjong_ranking/management/commands/update_ranking.py +++ b/src/mahjong_ranking/management/commands/updateranking.py @@ -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('forcerecalc') + 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) diff --git a/src/mahjong_ranking/models.py b/src/mahjong_ranking/models.py index b5d12d6..aa44006 100644 --- a/src/mahjong_ranking/models.py +++ b/src/mahjong_ranking/models.py @@ -362,11 +362,22 @@ class KyuDanRanking(models.Model): verbose_name = _(u'Kyū/Dan Ranking') verbose_name_plural = _(u'Kyū/Dan Rankings') + @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""" @@ -382,7 +393,6 @@ class KyuDanRanking(models.Model): self.wins_in_a_row = 0 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 diff --git a/src/mahjong_ranking/templates/mahjong_ranking/player_dan_score.html b/src/mahjong_ranking/templates/mahjong_ranking/player_dan_score.html index 2ef6db7..82aba0f 100755 --- a/src/mahjong_ranking/templates/mahjong_ranking/player_dan_score.html +++ b/src/mahjong_ranking/templates/mahjong_ranking/player_dan_score.html @@ -50,6 +50,10 @@ {% endfor %} + +{% if kyu_dan_ranking.legacy_date %} +

Frühere Dan Punkte vom {{ kyu_dan_ranking.legacy_date|date }}: {{kyu_dan_ranking.legacy_dan_points }}

+{% endif %} {% endblock %} {% block buttonbar %} diff --git a/src/mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html b/src/mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html index 7da226c..8e31486 100755 --- a/src/mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html +++ b/src/mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html @@ -35,8 +35,12 @@ {% if perms.mahjong_ranking.change_hanchan %} {% endif %} - + -{% endfor %} +{% endfor %} -{% endblock %} \ No newline at end of file +{% endblock %} + +{% block buttonbar %} + Download +{% endblock %} diff --git a/src/mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html b/src/mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html index 2444be7..a24677b 100755 --- a/src/mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html +++ b/src/mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html @@ -16,7 +16,7 @@ {% trans 'Placement' %} {% trans 'Players' %} {% trans 'Kyu Points' %} - + 1. @@ -45,6 +45,10 @@ {% endif %} -{% endfor %} +{% endfor %} -{% endblock %} \ No newline at end of file +{% endblock %} + +{% block buttonbar %} + Download +{% endblock %} diff --git a/src/mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html b/src/mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html index 6558fe0..e4aaa30 100755 --- a/src/mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html +++ b/src/mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html @@ -72,5 +72,8 @@ +{% endblock %} -{% endblock %} \ No newline at end of file +{% block buttonbar %} + Download +{% endblock %} diff --git a/src/mahjong_ranking/views.py b/src/mahjong_ranking/views.py index 3580dbb..275e99c 100644 --- a/src/mahjong_ranking/views.py +++ b/src/mahjong_ranking/views.py @@ -12,6 +12,7 @@ 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 @@ -28,56 +29,6 @@ KYU_DAN_ORDER = { # map sort URL args to Django ORM order_by args '-username': ('-user__username',) } -def getattr_recursive(obj, attr_string): - attr_list = attr_string.split('.') - return_value=None - for attr in attr_list: - print("Obj:", obj,'Attr:', attr) - return_value = getattr(obj, attr) - obj = return_value - return return_value - -def generate_sheet(workbook, title, columns_settings, object_list): - 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 object_list: - row += 1 - 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'] - - # set column widths - for settings in columns_settings: - ws.column_dimensions[settings['col']].width = settings['width'] - - class DeleteHanchan(EventDetailMixin, PermissionRequiredMixin, generic.DeleteView): @@ -235,48 +186,67 @@ class PlayerScore(LoginRequiredMixin, generic.ListView): return context def get_xlsx(self, request, *args, **kwargs): - from management.commands.export_ranking import geneate_excel self.object_list = self.get_queryset() - allow_empty = self.get_allow_empty() 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 = geneate_excel() - print(self.xlsx_columns) - generate_sheet(xlxs_workbook, - title=self.xlsx_filename, - columns_settings=self.xlsx_columns, - object_list=self.object_list - ) - xlxs_workbook.create_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' - xlsx_columns = ( - {'col': 'A', 'title': 'Beginn', 'attr': 'start', 'style': 'date', 'width': 14}, - {'col': 'B', 'title': 'Platzierung', 'attr': 'placement', 'style': 'int', 'width': 8}, - {'col': 'C', 'title': 'Spieler 1', 'attr': 'player1.username', 'style': 'content', 'width': 8}, - {'col': 'D', 'title': 'Punkte', 'attr': 'player1_game_score', 'style': 'int', 'width': 8}, - {'col': 'E', 'title': 'Spieler 2', 'attr': 'player2.username', 'style': 'content', 'width': 8}, - {'col': 'F', 'title': 'Punkte', 'attr': 'player2_game_score', 'style': 'int', 'width': 8}, - {'col': 'G', 'title': 'Spieler 3', 'attr': 'player3.username', 'style': 'content', 'width': 8}, - {'col': 'H', 'title': 'Punkte', 'attr': 'player3_game_score', 'style': 'int', 'width': 8}, - {'col': 'I', 'title': 'Spieler 4', 'attr': 'player4.username', 'style': 'content', 'width': 8}, - {'col': 'J', 'title': 'Punkte', 'attr': 'player4_game_score', 'style': 'int', 'width': 8}, - {'col': 'K', 'title': 'Dan Punkte', 'attr': 'dan_points', 'style': 'int', 'width': 8}, - {'col': 'L', 'title': 'Anmerkung', 'attr': 'comment', 'style': 'content', 'width': 8}, - ) def get_queryset(self): - kyu_dan_ranking = models.KyuDanRanking.objects.get(user=self.user) - self.xlsx_filename = "{username}_dan_score.xlsx".format( - username=self.user.username) - return models.Hanchan.objects.dan_hanchans(user=self.user, - since=kyu_dan_ranking.legacy_date) + self.kyu_dan_ranking = models.KyuDanRanking.objects.get(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': '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): @@ -292,9 +262,47 @@ class PlayerKyuScore(PlayerScore): template_name = 'mahjong_ranking/player_kyu_score.html' def get_queryset(self): - self.xlsx_filename = "{username}_kyu_score.xlsx".format( - username=self.user.username) - return models.Hanchan.objects.kyu_hanchans(self.user) + self.kyu_dan_ranking = models.KyuDanRanking.objects.get(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): @@ -311,10 +319,44 @@ class PlayerLadderScore(PlayerScore): def get_queryset(self, **kwargs): self.season = int(self.request.GET.get('season', date.today().year)) - self.xlsx_filename = "{username}_ladder ({season}).xlsx".format( - username=self.user.username, season=self.season) hanchan_list = models.Hanchan.objects.season_hanchans( user=self.user, 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 + ) diff --git a/src/maistar_ranking/migrations/0007_auto_20171214_1215.py b/src/maistar_ranking/migrations/0007_auto_20171214_1215.py new file mode 100644 index 0000000..66feede --- /dev/null +++ b/src/maistar_ranking/migrations/0007_auto_20171214_1215.py @@ -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), + ), + ] diff --git a/src/membership/models.py b/src/membership/models.py index 7157c5a..7b746d2 100644 --- a/src/membership/models.py +++ b/src/membership/models.py @@ -216,6 +216,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 diff --git a/src/membership/templates/membership/membership_detail.html b/src/membership/templates/membership/membership_detail.html index d04e6ba..d1af67c 100644 --- a/src/membership/templates/membership/membership_detail.html +++ b/src/membership/templates/membership/membership_detail.html @@ -35,7 +35,10 @@

Mahjong

    {% if kyu_dan_ranking.dan %} -
  • {{kyu_dan_ranking.dan}}. Dan: {{ kyu_dan_ranking.dan_points }} {% trans 'Points' %}
  • +
  • + {{kyu_dan_ranking.dan}}. Dan: {{ kyu_dan_ranking.dan_points }} {% trans 'Points' %} + ({% trans 'Maximum' %}: {{ kyu_dan_ranking.max_dan_points }}) +
  • {% elif kyu_dan_ranking.kyu%}
  • {{kyu_dan_ranking.kyu}}. Kyu: {{ kyu_dan_ranking.kyu_points }} {% trans 'Points' %}
  • {% endif %}