Squashed commit of the following:

commit bb5081a78b
Author: Xeniac <xeniac@posteo.at>
Date:   Thu Nov 23 22:02:40 2017 +0100

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

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

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

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

    add a latest method to query the latest x events

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

    add a latest method to query the latest x events

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

    Changed PlayerDanScore to only list non-legacy hanchans

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

    fixed import of MIN_HANCHANS_FOR_LADDER that moved to settings

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

    Updated docstrings for new since and until kwargs

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

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

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

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

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

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

    Added option to calcuclate rankings until a given datetime.
This commit is contained in:
2017-11-23 22:26:22 +01:00
parent fa6a81c710
commit d62f549a30
18 changed files with 530 additions and 180 deletions

View File

@@ -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'),
),
]

View File

@@ -31,6 +31,10 @@ class EventManager(models.Manager):
"""Returns all past events.""" """Returns all past events."""
return self.filter(start__lt=now()) 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): def upcoming(self, limit=None):
"""Returns the next 'limit' upcoming events. """Returns the next 'limit' upcoming events.

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,6 @@
"""Mixins for Events.""" """Mixins for Events."""
from django.http import Http404
from . import models from . import models
@@ -9,7 +11,6 @@ class EventArchiveMixin(object):
date_field = 'start' date_field = 'start'
make_object_list = True make_object_list = True
model = models.Event model = models.Event
ordering = ('start', 'end')
paginate_by = 15 paginate_by = 15
template_name = 'events/event_archive.html' template_name = 'events/event_archive.html'
@@ -40,3 +41,16 @@ class EventDetailMixin(object):
elif hasattr(self, 'object') and hasattr(self.object, 'event'): elif hasattr(self, 'object') and hasattr(self.object, 'event'):
context['event'] = self.object.event context['event'] = self.object.event
return context 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()

View File

@@ -30,7 +30,6 @@ class DeleteEventPhoto(PermissionRequiredMixin, mixins.EventDetailMixin,
class EventArchiveIndex(mixins.EventArchiveMixin, generic.ArchiveIndexView): class EventArchiveIndex(mixins.EventArchiveMixin, generic.ArchiveIndexView):
"""Index of the event archive, displays the upcoming events first.""" """Index of the event archive, displays the upcoming events first."""
allow_empty = True allow_empty = True
ordering = ('-start', '-end')
class EventArchiveMonth(mixins.EventArchiveMixin, generic.MonthArchiveView): class EventArchiveMonth(mixins.EventArchiveMixin, generic.MonthArchiveView):
@@ -73,7 +72,7 @@ class EventForm(PermissionRequiredMixin, mixins.EventDetailMixin,
if self.kwargs.get('pk') else models.Event() 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.""" """Display a overview of all event photo albums."""
template_name = 'events/photo_gallery.html' template_name = 'events/photo_gallery.html'
queryset = models.Event.objects.filter( queryset = models.Event.objects.filter(

View File

@@ -273,6 +273,7 @@ KYU_RANKS = (
DAN_ALLOW_DROP_DOWN = True DAN_ALLOW_DROP_DOWN = True
MIN_HANCHANS_FOR_LADDER = 5 MIN_HANCHANS_FOR_LADDER = 5
RANKING_EXPORT_PATH = path.join(PROJECT_PATH, 'backup', 'mahjong_ranking')
try: try:
from .local_settings import * # Ignore PyLintBear (W0401, W0614) from .local_settings import * # Ignore PyLintBear (W0401, W0614)

View File

@@ -1,11 +1,14 @@
"""Export Mahjong Rankings as excel files.""" """Export Mahjong Rankings as excel files."""
from datetime import date import os
from operator import itemgetter from datetime import date, time, datetime
import openpyxl import openpyxl
from django.conf import settings
from django.core.management.base import BaseCommand 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 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.border = DEFAULT_STYLE.border
FLOAT_STYLE.number_format = '#,##0.00' 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(): def geneate_excel():
"""Generate an excel .xlsx spreadsheet from json data of the kyu/dan """Generate an excel .xlsx spreadsheet from json data of the kyu/dan
rankings. rankings.
@@ -45,8 +65,9 @@ def geneate_excel():
workbook.add_named_style(DEFAULT_STYLE) workbook.add_named_style(DEFAULT_STYLE)
workbook.add_named_style(INT_STYLE) workbook.add_named_style(INT_STYLE)
workbook.add_named_style(FLOAT_STYLE) workbook.add_named_style(FLOAT_STYLE)
workbook.add_named_style(DATE_STYLE)
for sheet in workbook.worksheets: for sheet in workbook.worksheets:
print(sheet)
workbook.remove(sheet) workbook.remove(sheet)
return workbook return workbook
@@ -55,6 +76,8 @@ def generate_sheet(workbook, title, columns_settings, json_data):
row = 1 row = 1
ws = workbook.create_sheet() ws = workbook.create_sheet()
ws.title = title ws.title = title
ws.syncHorizontal = True
ws.filterMode = True
# setup print orientation # setup print orientation
ws.page_setup.orientation = ws.ORIENTATION_PORTRAIT 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'] ws.column_dimensions[settings['col']].width = settings['width']
def export_season_rankings(workbook): def export_season_rankings(workbook, until):
json_data = sorted(SeasonRanking.objects.json_data(), SeasonRanking.objects.update(until=until)
key=itemgetter('placement')) json_data = SeasonRanking.objects.json_data()
title = "Mahjong Ladder - {}".format(date.today().year) title = "Mahjong Ladder - {}".format(until.year)
columns_settings = ( columns_settings = (
{'col': 'A', 'title': 'Rang', 'attr': 'placement', 'style': 'int', {'col': 'A', 'title': 'Rang', 'attr': 'placement', 'style': 'int',
'width': 8}, 'width': 8},
@@ -101,7 +124,7 @@ def export_season_rankings(workbook):
'style': 'content', 'style': 'content',
'width': 25}, 'width': 25},
{'col': 'C', 'title': '⌀ Platz', 'attr': 'avg_placement', {'col': 'C', 'title': '⌀ Platz', 'attr': 'avg_placement',
'style': 'int', 'width': 8}, 'style': 'float', 'width': 8},
{'col': 'D', 'title': '⌀ Punkte', 'attr': 'avg_score', {'col': 'D', 'title': '⌀ Punkte', 'attr': 'avg_score',
'style': 'float', 'width': 12}, 'style': 'float', 'width': 12},
{'col': 'E', 'title': 'Hanchans', 'attr': 'hanchan_count', {'col': 'E', 'title': 'Hanchans', 'attr': 'hanchan_count',
@@ -111,7 +134,6 @@ def export_season_rankings(workbook):
{'col': 'G', 'title': 'Gewonnen', 'attr': 'won_hanchans', {'col': 'G', 'title': 'Gewonnen', 'attr': 'won_hanchans',
'style': 'int', 'width': 10}, 'style': 'int', 'width': 10},
) )
generate_sheet( generate_sheet(
workbook=workbook, workbook=workbook,
title=title, title=title,
@@ -119,7 +141,8 @@ def export_season_rankings(workbook):
json_data=json_data) 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() json_data = KyuDanRanking.objects.json_data()
title = "Kyū & Dan Rankings" title = "Kyū & Dan Rankings"
columns_settings = ( columns_settings = (
@@ -136,7 +159,9 @@ def export_kyu_dan_rankings(workbook):
{'col': 'F', 'title': 'Gut', 'attr': 'good_hanchans', {'col': 'F', 'title': 'Gut', 'attr': 'good_hanchans',
'style': 'int', 'width': 5}, 'style': 'int', 'width': 5},
{'col': 'G', 'title': 'Gewonnen', 'attr': 'won_hanchans', {'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( generate_sheet(
workbook=workbook, workbook=workbook,
@@ -147,12 +172,43 @@ def export_kyu_dan_rankings(workbook):
class Command(BaseCommand): class Command(BaseCommand):
"""Exports the SeasonRankings""" """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): def handle(self, *args, **options):
"""Exports the current ladder ranking in a spreadsheet. """Exports the current ladder ranking in a spreadsheet.
This is useful as a backup in form of a hardcopy.""" 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() workbook = geneate_excel()
export_season_rankings(workbook) export_season_rankings(workbook, until=self.until)
export_kyu_dan_rankings(workbook) export_kyu_dan_rankings(workbook, until=self.until)
workbook.save('sample.x') os.makedirs(settings.RANKING_EXPORT_PATH, exist_ok=True)
workbook.save('mahjong_rankings_{}.xlsx'.format(str(date.today()))) workbook.save(self.filename)
if options['mail']:
self.send_mail(options['mail'])
def send_mail(self, recipients):
mail = EmailMessage(
subject='Mahjong Rankings vom {:%d.%m.%Y}'.format(self.until),
body=MAIL_BODY,
from_email=settings.DEFAULT_FROM_EMAIL,
to=recipients)
mail.attach_file(self.filename)
mail.send()

View File

@@ -0,0 +1,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()

View File

@@ -5,27 +5,29 @@ Recalculate Mahjong Rankings...
""" """
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from datetime import date, datetime, time
from mahjong_ranking import LOGGER
from mahjong_ranking import models from mahjong_ranking import models
from django.utils.dateparse import parse_date
from django.utils import timezone
class Command(BaseCommand): class Command(BaseCommand):
""" Recalculate all Kyu/Dan Rankings """ """ Recalculate all Kyu/Dan Rankings """
help = "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): def handle(self, *args, **options):
old_attr = {'dan': None, 'dan_points': None, since = options.get('since', None)
'kyu': None, 'kyu_points': None, 'won_hanchans': None, until = options.get('until', None)
'good_hanchans': None, 'hanchan_count': None} force_recalc = options.get('forecerecalc', False)
for ranking in models.KyuDanRanking.objects.all(): if isinstance(since, date):
old_attr = {attr: getattr(ranking, attr) for attr in old_attr.keys()} since = datetime.combine(since, time(0, 0, 0))
ranking.recalculate() since = timezone.make_aware(since)
for attr, old_value in old_attr.items(): if isinstance(until, date):
if getattr(ranking, attr) != old_value: until = datetime.combine(until, time(23, 59, 59))
LOGGER.warning( until = timezone.make_aware(until)
"%(user)s recalc shows differences in %(attr)s! old: %(old)d, new: %(new)d", models.KyuDanRanking.objects.update(since=since, until=until, force_recalc=force_recalc)
{'user': ranking.user, 'attr': attr,
'old': old_value, 'new': getattr(ranking, attr)}
)

View File

@@ -1,7 +1,8 @@
"""ObjectManagers for the Django Models used in the Mahjong-Ranking.""" """ObjectManagers for the Django Models used in the Mahjong-Ranking."""
from datetime import date from datetime import date
from . import LOGGER
from django.db import models from django.db import models
from django.conf import settings
class HanchanManager(models.Manager): class HanchanManager(models.Manager):
@@ -12,23 +13,31 @@ class HanchanManager(models.Manager):
""" """
use_for_related_fields = True 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. """ 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. :param filter_args: To add specific arguments to the Django filter.
:return: QuerySet Object :return: QuerySet Object
""" """
if user: if user:
return self.user_hanchans(user, confirmed=True, **filter_args) return self.user_hanchans(user, confirmed=True, until=until,
else: **filter_args)
return self.filter(confirmed=True, **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 """ Return all Hanchans where a specific user has participated and had
gain dan points and make his gamestats availabale. gain dan points and make his gamestats availabale.
: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 filter_args: To add specific arguments to the Django filter. :param filter_args: To add specific arguments to the Django filter.
:return: QuerySet Object :return: QuerySet Object
""" """
@@ -38,15 +47,18 @@ class HanchanManager(models.Manager):
models.Q(player3=user, player3_dan_points__isnull=False) | models.Q(player3=user, player3_dan_points__isnull=False) |
models.Q(player4=user, player4_dan_points__isnull=False) models.Q(player4=user, player4_dan_points__isnull=False)
).filter(confirmed=True, **filter_args) ).filter(confirmed=True, **filter_args)
if since:
queryset = queryset.filter(start__gt=since)
queryset = queryset.select_related().order_by('-start') 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 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 """ Return all Hanchans where a specific user has participated and had
gain kyū points and make his gamestats availabale. gain kyū points and make his gamestats availabale.
: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 filter_args: To add specific arguments to the Django filter. :param filter_args: To add specific arguments to the Django filter.
:return: QuerySet Object :return: QuerySet Object
""" """
@@ -56,25 +68,30 @@ class HanchanManager(models.Manager):
models.Q(player3=user, player3_kyu_points__isnull=False) | models.Q(player3=user, player3_kyu_points__isnull=False) |
models.Q(player4=user, player4_kyu_points__isnull=False) models.Q(player4=user, player4_kyu_points__isnull=False)
).filter(confirmed=True, **filter_args) ).filter(confirmed=True, **filter_args)
if since:
queryset = queryset.filter(start__gt=since)
queryset = queryset.select_related().order_by('-start') 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 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. """Return all Hanchans that belong to a given or the current season.
:param user: Only return Hanchans where this user participated. :param user: Only return Hanchans where this user participated.
:param season: the year of the wanted season, current year if None. :param season: the year of the wanted season, current year if None.
:return: QuerySet Object :return: QuerySet Object
""" """
season = season or date.today().year try:
return self.confirmed_hanchans(user=user, season=season) 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. """Return all Hanchans where a specific user has participated.
:param user: Return Hanchans where this user 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. :param filter_args: To add specific arguments to the Django filter.
:return: a QuerySet Object :return: a QuerySet Object
""" """
@@ -82,15 +99,16 @@ class HanchanManager(models.Manager):
models.Q(player1=user) | models.Q(player2=user) | models.Q(player1=user) | models.Q(player2=user) |
models.Q(player3=user) | models.Q(player4=user) models.Q(player3=user) | models.Q(player4=user)
) )
if since:
queryset = queryset.filter(start__gte=since, **filter_args)
else:
queryset = queryset.filter(**filter_args) queryset = queryset.filter(**filter_args)
if since:
queryset = queryset.filter(start__gte=since)
if until:
queryset = queryset.filter(start__lte=until)
queryset = queryset.select_related().order_by('-start') 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 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. """ Return all Hanchans that have been set to unconfirmed.
:param user: Only return Hanchans where this user participated. :param user: Only return Hanchans where this user participated.
@@ -158,8 +176,27 @@ class SeasonRankingManager(models.Manager):
}) })
return json_data 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): def json_data(self):
""" Get all Rankings for a given Season and return them as a list of """ Get all Rankings for a given Season and return them as a list of
dict objects, suitable for JSON exports and other processings. dict objects, suitable for JSON exports and other processings.
@@ -172,9 +209,12 @@ class KyuDanRankingManager(models.Manager):
values = values.values('user_id', 'user__username', values = values.values('user_id', 'user__username',
'user__first_name', 'user__last_name', 'user__first_name', 'user__last_name',
'dan', 'dan_points', 'kyu', 'kyu_points', '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: for user in values:
if user['dan']: if user['hanchan_count'] == 0:
continue
elif user['dan']:
rank = '{}. Dan'.format(user['dan']) rank = '{}. Dan'.format(user['dan'])
points = user['dan_points'] points = user['dan_points']
else: else:
@@ -183,11 +223,31 @@ class KyuDanRankingManager(models.Manager):
json_data.append({ json_data.append({
'user_id': user['user_id'], 'user_id': user['user_id'],
'username': user['user__username'], '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, 'rank': rank,
'points': points, 'points': points,
'hanchan_count': user['hanchan_count'], 'hanchan_count': user['hanchan_count'],
'good_hanchans': user['good_hanchans'], '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 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}
)

View File

@@ -1,8 +1,7 @@
"""Middleware to defer slow denormalization at the end of a request.""" """Middleware to defer slow denormalization at the end of a request."""
from django.core.cache import cache from django.core.cache import cache
from mahjong_ranking import models from mahjong_ranking import models
from . import LOGGER, MIN_HANCHANS_FOR_LADDER from . import LOGGER
class DenormalizationUpdateMiddleware(object): # Ignore PyLintBear (R0903) 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() user_id, hanchan_start = kyu_dan_ranking_queue.pop()
kyu_dan_ranking = models.KyuDanRanking.objects.get_or_create( kyu_dan_ranking = models.KyuDanRanking.objects.get_or_create(
user_id=user_id)[0] 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) cache.set('kyu_dan_ranking_queue', set(), 360)
ladder_ranking_queue = cache.get('ladder_ranking_queue', set()) ladder_ranking_queue = cache.get('ladder_ranking_queue', set())
@@ -58,12 +57,5 @@ class DenormalizationUpdateMiddleware(object): # Ignore PyLintBear (R0903)
for season in season_queue: for season in season_queue:
LOGGER.info(u'Recalculate placements for Season %d', season) LOGGER.info(u'Recalculate placements for Season %d', season)
season_rankings = models.SeasonRanking.objects.filter( models.SeasonRanking.objects.update(season=season)
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
return response return response

View File

@@ -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),
),
]

View File

@@ -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

View File

@@ -5,6 +5,8 @@
from __future__ import division from __future__ import division
from datetime import datetime, time
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
@@ -56,7 +58,7 @@ class EventRanking(models.Model):
) )
sum_placement = 0.0 sum_placement = 0.0
sum_score = 0.0 sum_score = 0.0
event_hanchans = Hanchan.objects.confirmed_hanchans( event_hanchans = Hanchan.objects.confirmed(
user=self.user_id, user=self.user_id,
event=self.event_id event=self.event_id
) )
@@ -345,10 +347,10 @@ class KyuDanRanking(models.Model):
legacy_hanchan_count = models.PositiveIntegerField(default=0) legacy_hanchan_count = models.PositiveIntegerField(default=0)
legacy_dan_points = models.PositiveIntegerField(default=0) legacy_dan_points = models.PositiveIntegerField(default=0)
legacy_kyu_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() objects = managers.KyuDanRankingManager()
class Meta(object): class Meta(object):
ordering = ('-dan_points', 'dan', '-kyu_points') ordering = ('-dan_points', 'dan', '-kyu_points')
verbose_name = _(u'Kyū/Dan Ranking') verbose_name = _(u'Kyū/Dan Ranking')
@@ -436,11 +438,19 @@ class KyuDanRanking(models.Model):
else: else:
return reverse('player-kyu-score', args=[self.user.username]) 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 Fetches all valid Hanchans from this Player and recalculates his
Kyu/Dan Ranking. Kyu/Dan Ranking.
""" """
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 = None
self.dan_points = self.legacy_dan_points or 0 self.dan_points = self.legacy_dan_points or 0
self.kyu = None self.kyu = None
@@ -449,23 +459,27 @@ class KyuDanRanking(models.Model):
self.good_hanchans = 0 self.good_hanchans = 0
self.won_hanchans = 0 self.won_hanchans = 0
self.update_rank() self.update_rank()
self.last_hanchan_date = None
LOGGER.info(
"recalculating Kyu/Dan points for %s since %s...",
self.user, str(hanchan_start)
)
valid_hanchans = Hanchan.objects.confirmed_hanchans(
user=self.user).order_by('start')
if self.legacy_date: if self.legacy_date:
valid_hanchans = valid_hanchans.filter(start__date__gte=self.legacy_date) since = timezone.make_aware(
datetime.combine(self.legacy_date, time(0, 0, 0)))
""" TODO: Hanchan Punkte nur neu berechnen wenn sie nach hachan_start else:
lagen. Es müssen aber alle durch die Schleife rennen, damit die Punkte since = None
richtig gezählt werden.""" 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 %(user)s since %(since)s...",
{'user': self.user, 'since': str(since)}
)
self.hanchan_count += valid_hanchans.count() self.hanchan_count += valid_hanchans.count()
for hanchan in valid_hanchans: for hanchan in valid_hanchans:
hanchan.get_playerdata(self.user) 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.dan_points += hanchan.dan_points or 0
self.kyu_points += hanchan.kyu_points or 0 self.kyu_points += hanchan.kyu_points or 0
self.update_rank() self.update_rank()
@@ -482,6 +496,7 @@ class KyuDanRanking(models.Model):
hanchan.save(recalculate=False) hanchan.save(recalculate=False)
self.won_hanchans += 1 if hanchan.placement == 1 else 0 self.won_hanchans += 1 if hanchan.placement == 1 else 0
self.good_hanchans += 1 if hanchan.placement == 2 else 0 self.good_hanchans += 1 if hanchan.placement == 2 else 0
self.last_hanchan_date = hanchan.start
LOGGER.debug( LOGGER.debug(
'id: %(id)d, start: %(start)s, placement: %(placement)d, ' 'id: %(id)d, start: %(start)s, placement: %(placement)d, '
'score: %(score)d, kyu points: %(kyu_points)d, dan points: ' 'score: %(score)d, kyu points: %(kyu_points)d, dan points: '
@@ -494,6 +509,7 @@ class KyuDanRanking(models.Model):
) )
self.save(force_update=True) self.save(force_update=True)
def update_hanchan_points(self, hanchan): def update_hanchan_points(self, hanchan):
""" """
Berechne die Kyu bzw. Dan Punkte für eine Hanchan neu. 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) hanchan.kyu_points -= (self.kyu_points + hanchan.kyu_points)
self.kyu_points += hanchan.kyu_points self.kyu_points += hanchan.kyu_points
# TODO: Merkwürdige Methode die zwar funktioniert aber nicht sehr # TODO: Merkwürdige Methode die zwar funktioniert aber nicht sehr
# aussagekräfig ist. Überarbeiten? # aussagekräfig ist. Überarbeiten?
def update_rank(self): def update_rank(self):
@@ -593,9 +610,9 @@ class SeasonRanking(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('player-ladder-score', args=[self.user.username]) return reverse('player-ladder-score', args=[self.user.username])
def recalculate(self): def recalculate(self, until=None):
season_hanchans = Hanchan.objects.season_hanchans( season_hanchans = Hanchan.objects.season_hanchans(
user=self.user, season=self.season) user=self.user, season=self.season, until=until)
sum_placement = 0 sum_placement = 0
sum_score = 0 sum_score = 0
self.placement = None self.placement = None

View File

@@ -34,7 +34,7 @@ class KyuDanTest(TestCase):
for ranking in KyuDanRanking.objects.all(): for ranking in KyuDanRanking.objects.all():
original = {a: getattr(ranking, a) for a in self.equal_attrs} original = {a: getattr(ranking, a) for a in self.equal_attrs}
ranking.recalculate() ranking.calculate()
for attr in self.equal_attrs: for attr in self.equal_attrs:
self.assertEqual( self.assertEqual(
original[attr], original[attr],
@@ -54,7 +54,7 @@ class KyuDanTest(TestCase):
for ranking in KyuDanRanking.objects.all(): for ranking in KyuDanRanking.objects.all():
original = {a: getattr(ranking, a) for a in self.equal_attrs} 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, user=ranking.user,
since=ranking.legacy_date since=ranking.legacy_date
) )
@@ -62,7 +62,7 @@ class KyuDanTest(TestCase):
continue continue
rnd = random.randrange(confirmed_hanchans.count()) rnd = random.randrange(confirmed_hanchans.count())
since = confirmed_hanchans[rnd].start since = confirmed_hanchans[rnd].start
ranking.recalculate(hanchan_start=since) ranking.calculate(since=since)
for attr in self.equal_attrs: for attr in self.equal_attrs:
self.assertEqual( self.assertEqual(
original[attr], original[attr],
@@ -86,7 +86,7 @@ class KyuDanTest(TestCase):
'dan_points': ranking.legacy_dan_points or 0, 'dan_points': ranking.legacy_dan_points or 0,
'kyu_points': ranking.legacy_kyu_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, user=ranking.user,
since=ranking.legacy_date since=ranking.legacy_date
) )

View File

@@ -11,11 +11,11 @@ from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views import generic from django.views import generic
from events.models import Event
from events.mixins import EventDetailMixin from events.mixins import EventDetailMixin
from . import forms, models 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'),
'-full_name': ('-user__last_name', '-user__first_name'), '-full_name': ('-user__last_name', '-user__first_name'),
'+hanchan_count': ('hanchan_count',), '+hanchan_count': ('hanchan_count',),
@@ -31,16 +31,17 @@ kyu_dan_order = {
class DeleteHanchan(EventDetailMixin, PermissionRequiredMixin, class DeleteHanchan(EventDetailMixin, PermissionRequiredMixin,
generic.DeleteView): generic.DeleteView):
""" """Deletes a Hanchan if confimration has been answerd with 'yes'."""
Fragt zuerst nach, ob die Hanchan wirklich gelöscht werden soll.
Wir die Frage mit "Ja" beantwortet, wird die die Hanchan gelöscht.
"""
form_class = forms.HanchanForm form_class = forms.HanchanForm
model = models.Hanchan model = models.Hanchan
permission_required = 'mahjong_ranking.delete_hanchan' permission_required = 'mahjong_ranking.delete_hanchan'
pk_url_kwarg = 'hanchan' pk_url_kwarg = 'hanchan'
def get_success_url(self): 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', return reverse('event-hanchan-list',
kwargs={'event': self.object.event.pk}) kwargs={'event': self.object.event.pk})
@@ -48,32 +49,30 @@ class DeleteHanchan(EventDetailMixin, PermissionRequiredMixin,
class HanchanForm(SuccessMessageMixin, EventDetailMixin, class HanchanForm(SuccessMessageMixin, EventDetailMixin,
PermissionRequiredMixin, generic.UpdateView): PermissionRequiredMixin, generic.UpdateView):
""" """
Ein Formular um neue Hanchans anzulegen, bzw. eine bestehende zu A Form to add a new or edit an existing Hanchan.
bearbeitsen
""" """
form_class = forms.HanchanForm form_class = forms.HanchanForm
model = models.Hanchan model = models.Hanchan
permission_required = 'mahjong_ranking.add_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): def get_form_class(self):
""" """
Users with edit Persmission will see the AdminForm to confirm Users with hanchan edit persmission can also un-/confirm hanchans.
unconfirmed Hanchans. :return: forms.HanchanForm, or forms.HanchanAdminForm
""" """
if self.request.user.has_perm('mahjong_ranking.change_hanchan'): return forms.HanchanAdminForm if self.request.user.has_perm(
return forms.HanchanAdminForm 'mahjong_ranking.change_hanchan') else forms.HanchanForm
else:
return forms.HanchanForm
def get_object(self, queryset=None): 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( if self.kwargs.get('hanchan') and self.request.user.has_perm(
'mahjong_ranking.change_hanchan'): '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 self.event = hanchan.event
elif self.kwargs.get('event'): elif self.kwargs.get('event'):
self.event = models.Event.objects.get(id=self.kwargs['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}) return reverse('add-hanchan-form', kwargs={'event': self.event.pk})
def get_success_message(self, cleaned_data): 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'): if self.kwargs.get('hanchan'):
return _('%s has been updated successfully.') % self.object return _('%s has been updated successfully.') % self.object
else: else:
@@ -103,72 +107,36 @@ class HanchanForm(SuccessMessageMixin, EventDetailMixin,
class EventHanchanList(EventDetailMixin, generic.ListView): class EventHanchanList(EventDetailMixin, generic.ListView):
""" "List all hanchans played on a given event."
Auflistung aller Hanchan die während der Veranstaltung gespielt wurden.
"""
model = models.Hanchan model = models.Hanchan
template_name = 'mahjong_ranking/eventhanchan_list.html' 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): class EventRankingList(EventDetailMixin, generic.ListView):
""" """Display the event ranking for the given event."""
Anzeige des Eventrankings, daß erstellt wurde falls der Termin als internes
Turnier markiert wurde.
"""
model = models.EventRanking 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): class KyuDanRankingList(MahjongMixin, generic.ListView):
""" """List all Players with an Kyu or Dan score. """
Anzeige aller Spiele mit ihrem Kyu bzw Dan Grad.
"""
default_order = '-score' default_order = '-score'
order_by = '' order_by = ''
paginate_by = 25 paginate_by = 25
def dispatch(self, request, *args, **kwargs): 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) 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): 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() return queryset.select_related()
@@ -223,14 +191,16 @@ class PlayerDanScore(PlayerScore):
template_name = 'mahjong_ranking/player_dan_score.html' template_name = 'mahjong_ranking/player_dan_score.html'
def get_queryset(self): 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): class PlayerInvalidScore(PlayerScore):
template_name = 'mahjong_ranking/player_invalid_score.html' template_name = 'mahjong_ranking/player_invalid_score.html'
def get_queryset(self): def get_queryset(self):
return models.Hanchan.objects.unconfirmed_hanchans(user=self.user) return models.Hanchan.objects.unconfirmed(user=self.user)
class PlayerKyuScore(PlayerScore): class PlayerKyuScore(PlayerScore):

View File

@@ -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')},
),
]

View File

@@ -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'),
),
]