17 Commits

Author SHA1 Message Date
0b2e040fc9 * Kommentare wenn Dan/Kyu Punktabzüge verringert werden um nicht unter
0 zu fallen.
* Neue Middleware die REMOTE_IP aus dem X-Forward-For Header setzt.
  Damit funktioniert das Kommentarsystem nun auch hinter nginx.
2017-12-29 10:03:08 +01:00
fdbf819092 XLSX Export vereinheitlicht.
Spieler Hanchanlisten können nun als XLSX exportiert werden.
Anpassungen in den Einstellungen für die parametisierten Kyu/Dan Berechnung.
2017-12-26 21:45:39 +01:00
9f6fffa4f4 Noch mehr Einstellungen für Kasu Ranking, um es an das neue Dan System anpassen zu können. 2017-12-22 10:54:11 +01:00
b7fab97715 Diverse Umbauarbeiten für das neue Ranking. 2017-12-22 10:51:20 +01:00
c030a31e2b Pinned Django on < 2.0 for better compatibility.
Mainlined traslation code for better DRY workflow.
Fixed the EventDetail Mixin.
2017-12-07 22:54:18 +01:00
ade2a568f7 added on_delete in models an migrations for django 2.0
compatibility.
2017-12-07 22:08:47 +01:00
c5781246fe Added a setting where the exported excel files should be stored.
Added a option to send the exported excel as mail attachment.
2017-12-07 09:40:35 +01:00
bb5081a78b Added a setting where the exported excel files should be stored.
Added a option to send the exported excel as mail attachment.
2017-11-23 22:02:40 +01:00
854fd38740 Fixed: enumerate the Seasonrankings starting with 1
Fixed: Logging error when a value changed from/to None
2017-11-23 22:01:38 +01:00
6de1ecb102 add a latest method to query the latest x events 2017-11-23 14:15:36 +01:00
bf12060c3b add a latest method to query the latest x events 2017-11-23 14:15:12 +01:00
5ad628f33a Changed PlayerDanScore to only list non-legacy hanchans 2017-11-20 07:47:47 +01:00
36272c60d6 fixed import of MIN_HANCHANS_FOR_LADDER that moved to settings 2017-11-20 07:42:44 +01:00
c428f6ed1f Updated docstrings for new since and until kwargs 2017-11-20 07:41:04 +01:00
9276e97c36 added a since parameter to the hanchan queries to return only hanchans since the give date and time 2017-11-20 07:33:54 +01:00
fd244f10e8 new command: resetdanranking YYYY-MM-DD, sets every dan player to 1st dan with zero dan_points at the given date. 2017-11-19 16:55:10 +01:00
0a45cf1fd8 added new fields to KyuDanRanking that allow to pick up the calculation from the last state of the KyuDanRanking.
last_hanchan_date: it contains the start of the latest hanchan content for this players ranking.
wins_in_row: to save the currents wins in a row

Added option to calcuclate rankings until a given datetime.
2017-11-19 16:14:59 +01:00
54 changed files with 5024 additions and 620 deletions

1
.gitignore vendored
View File

@@ -62,6 +62,7 @@ docs/_build/
target/ target/
#Django Development #Django Development
backup/
/bower_components/ /bower_components/
/media/ /media/
/node_modules/ /node_modules/

5
TODO
View File

@@ -766,11 +766,6 @@ src/mahjong_ranking/models.py
| | [NORMAL] PyLintBear (W0201): | | [NORMAL] PyLintBear (W0201):
| | W0201 - Attribute 'kyu_points' defined outside __init__ | | W0201 - Attribute 'kyu_points' defined outside __init__
src/mahjong_ranking/models.py
| 330| class·KyuDanRanking(models.Model):
| | [NORMAL] PyLintBear (W5102):
| | W5102 - Found __unicode__ method on model (KyuDanRanking). Python3 uses __str__.
src/mahjong_ranking/models.py src/mahjong_ranking/models.py
| 330| class·KyuDanRanking(models.Model): | 330| class·KyuDanRanking(models.Model):
| | [INFO] PyLintBear (R0902): | | [INFO] PyLintBear (R0902):

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
SSH_LOGIN="kasu@s21.wservices.ch" SSH_LOGIN="kasu@s21.wservices.ch"
SYNC_ASSESTS="requirements" SYNC_ASSESTS="requirements static"
SYNC_SOURCECODE="src" SYNC_SOURCECODE="src"
EXCLUDE_FILES="*.pyc" EXCLUDE_FILES="*.pyc"
@@ -19,5 +19,5 @@ rsync -r --copy-links --delete ${SYNC_SOURCECODE} ${SSH_LOGIN}:~/ --exclude 'src
echo "Rebuild and reload django..." echo "Rebuild and reload django..."
ssh ${SSH_LOGIN} "rm src/kasu/settings/development.*" ssh ${SSH_LOGIN} "rm src/kasu/settings/development.*"
ssh ${SSH_LOGIN} "virtualenv/bin/python ~/src/manage.py collectstatic -l --noinput -v1" ssh ${SSH_LOGIN} "~/virtualenv/bin/python ~/src/manage.py collectstatic -l --noinput -v1"
ssh ${SSH_LOGIN} "~/init/kasu restart" ssh ${SSH_LOGIN} "~/init/kasu restart"

View File

@@ -1,5 +1,5 @@
beautifulsoup4 beautifulsoup4
django django < 2.0
django-appconf django-appconf
django-ckeditor django-ckeditor
django-contrib-comments django-contrib-comments

View File

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

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

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

View File

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

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.

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

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,20 @@ 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
"""
if self.model == models.Event:
self.event = models.Event.objects.get(pk=self.kwargs['pk'])
queryset = self.model.objects.all()
else:
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

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

View File

@@ -3,7 +3,7 @@ from datetime import timedelta
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.urlresolvers import reverse from django.urls import reverse
from django.db.models import Q from django.db.models import Q
from django.http import HttpResponse, Http404 from django.http import HttpResponse, Http404
from django.shortcuts import redirect from django.shortcuts import redirect
@@ -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

@@ -80,6 +80,7 @@ MIDDLEWARE_CLASSES = [
'django.middleware.locale.LocaleMiddleware', 'django.middleware.locale.LocaleMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'utils.middleware.SetRemoteAddrFromForwardedFor',
'mahjong_ranking.middleware.DenormalizationUpdateMiddleware', 'mahjong_ranking.middleware.DenormalizationUpdateMiddleware',
] ]
@@ -229,7 +230,7 @@ LOGGING = {
'loggers': { 'loggers': {
'django': { 'django': {
'handlers': ['console'], 'handlers': ['console'],
'level': 'INFO', 'level': 'DEBUG',
'propagate': True, 'propagate': True,
}, },
'django.request': { 'django.request': {
@@ -246,6 +247,22 @@ LOGGING = {
} }
} }
################################
# Settings for mahjong_ranking #
################################
MIN_HANCHANS_FOR_LADDER = 5
RANKING_EXPORT_PATH = path.join(PROJECT_PATH, 'backup', 'mahjong_ranking')
# Old Tournament System
TOURNAMENT_POINT_SYSTEM = True
TOURNAMENT_WIN_BONUSPOINTS = 4
TOURNAMENT_FLAWLESS_VICTORY_BONUSPOINTS = 8
# Old Dan System
DAN_3_WINS_IN_A_ROW = True
DAN_ALLOW_DROP_DOWN = True
DAN_RANKS = ( DAN_RANKS = (
(80, 9), (80, 9),
(70, 8), (70, 8),
@@ -255,7 +272,7 @@ DAN_RANKS = (
(30, 4), (30, 4),
(20, 3), (20, 3),
(10, 2), (10, 2),
(0, 1), (-1, 1),
) )
KYU_RANKS = ( KYU_RANKS = (
@@ -268,12 +285,9 @@ KYU_RANKS = (
(15, 7), (15, 7),
(10, 8), (10, 8),
(5, 9), (5, 9),
(0, 10), (-1, 10),
) )
DAN_ALLOW_DROP_DOWN = True
MIN_HANCHANS_FOR_LADDER = 5
try: try:
from .local_settings import * # Ignore PyLintBear (W0401, W0614) from .local_settings import * # Ignore PyLintBear (W0401, W0614)
except ImportError: except ImportError:

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,7 +32,7 @@ urlpatterns = [ # Ignore PyLintBear (C0103)
url(r'^add_page/(?P<path>[\+\.\-\d\w\/]+)/$', url(r'^add_page/(?P<path>[\+\.\-\d\w\/]+)/$',
views.PageAddForm.as_view(), name='add-page'), views.PageAddForm.as_view(), name='add-page'),
url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 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'^ckeditor/', include('ckeditor_uploader.urls')),
url(r'^comments/', include('django_comments.urls')), url(r'^comments/', include('django_comments.urls')),
url(r'^edit_page/(?P<path>[\+\.\-\d\w\/]+)/$', url(r'^edit_page/(?P<path>[\+\.\-\d\w\/]+)/$',

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

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

View File

@@ -21,8 +21,6 @@ def recalculate(modeladmin, request, queryset): # Ignore PyLintBear (W0613)
for ladder_ranking in queryset: for ladder_ranking in queryset:
set_dirty(user=ladder_ranking.user_id, set_dirty(user=ladder_ranking.user_id,
season=ladder_ranking.season) season=ladder_ranking.season)
recalculate.short_description = _("Recalculate") recalculate.short_description = _("Recalculate")

View File

@@ -1,158 +0,0 @@
"""Export Mahjong Rankings as excel files."""
from datetime import date
from operator import itemgetter
import openpyxl
from django.core.management.base import BaseCommand
from openpyxl.styles import Border
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'
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)
for sheet in workbook.worksheets:
print(sheet)
workbook.remove(sheet)
return workbook
def generate_sheet(workbook, title, columns_settings, json_data):
row = 1
ws = workbook.create_sheet()
ws.title = title
# 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):
json_data = sorted(SeasonRanking.objects.json_data(),
key=itemgetter('placement'))
title = "Mahjong Ladder - {}".format(date.today().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': 'int', '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):
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': 10},
)
generate_sheet(
workbook=workbook,
title=title,
columns_settings=columns_settings,
json_data=json_data)
class Command(BaseCommand):
"""Exports the SeasonRankings"""
def handle(self, *args, **options):
"""Exports the current ladder ranking in a spreadsheet.
This is useful as a backup in form of a hardcopy."""
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())))

View File

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

View File

@@ -0,0 +1,38 @@
"""
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):
legacy_attrs = [ key for key in models.KyuDanRanking.__dict__.keys()
if key.startswith('legacy') ]
legacy_attrs.remove('legacy_date')
reset_date = timezone.make_aware(datetime.combine(
options.get('reset_date'), time(23, 59, 59)))
models.KyuDanRanking.objects.update(until=reset_date, force_recalc=True)
for ranking in models.KyuDanRanking.objects.filter(dan__gt=0):
print(ranking)
ranking.dan = 1
ranking.dan_points = 0
ranking.kyu = None
ranking.kyu_points = 0
ranking.wins_in_a_row = 0
ranking.legacy_date = reset_date.date()
for legacy_attr in legacy_attrs:
attr = legacy_attr.split("_", maxsplit=1)[1]
print(ranking, legacy_attr, attr, getattr(ranking, attr))
setattr(ranking, legacy_attr, getattr(ranking, attr))
ranking.save()

View File

@@ -1,31 +0,0 @@
# -*- coding: utf-8 -*-
"""
Recalculate Mahjong Rankings...
"""
from django.core.management.base import BaseCommand
from mahjong_ranking import LOGGER
from mahjong_ranking import models
class Command(BaseCommand):
""" Recalculate all Kyu/Dan Rankings """
help = "Recalculate all Kyu/Dan Rankings"
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)}
)

View File

@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
"""
Recalculate Mahjong Rankings...
"""
from datetime import date, datetime, time
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 the Kyu/Dan Rankings"
def add_arguments(self, parser):
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('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)

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)
) )
queryset = queryset.filter(**filter_args)
if since: if since:
queryset = queryset.filter(start__gte=since, **filter_args) queryset = queryset.filter(start__gte=since)
else: if until:
queryset = queryset.filter(**filter_args) 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

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

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

View File

@@ -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,10 +5,12 @@
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.db import models from django.db import models
from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@@ -27,8 +29,8 @@ class EventRanking(models.Model):
Sie beschränken sich aber auf einen Event und werden nur dann angestossen, Sie beschränken sich aber auf einen Event und werden nur dann angestossen,
wenn der Event als Turnier markiert wurde. wenn der Event als Turnier markiert wurde.
""" """
user = models.ForeignKey(settings.AUTH_USER_MODEL) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
event = models.ForeignKey(Event) event = models.ForeignKey(Event, on_delete=models.CASCADE)
placement = models.PositiveIntegerField(blank=True, null=True) placement = models.PositiveIntegerField(blank=True, null=True)
avg_placement = models.FloatField(default=4) avg_placement = models.FloatField(default=4)
avg_score = models.FloatField(default=0) avg_score = models.FloatField(default=0)
@@ -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
) )
@@ -84,7 +86,7 @@ class Hanchan(models.Model):
Es werden aber noch andere Tests durchgeführt, ob sie gültig ist. Es werden aber noch andere Tests durchgeführt, ob sie gültig ist.
Außerdem gehört jede Hanchan zu einer Veranstaltung. Außerdem gehört jede Hanchan zu einer Veranstaltung.
""" """
event = models.ForeignKey(Event) event = models.ForeignKey(Event, on_delete=models.CASCADE)
start = models.DateTimeField( start = models.DateTimeField(
_('Start'), _('Start'),
help_text=_('This is crucial to get the right Hanchans that scores') help_text=_('This is crucial to get the right Hanchans that scores')
@@ -92,7 +94,7 @@ class Hanchan(models.Model):
player1 = models.ForeignKey( player1 = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
on_delete=models.CASCADE, on_delete=models.PROTECT,
related_name='user_hanchan+', related_name='user_hanchan+',
verbose_name=_('Player 1')) verbose_name=_('Player 1'))
player1_input_score = models.IntegerField(_('Score')) player1_input_score = models.IntegerField(_('Score'))
@@ -111,7 +113,7 @@ class Hanchan(models.Model):
player2 = models.ForeignKey( player2 = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
on_delete=models.CASCADE, on_delete=models.PROTECT,
related_name='user_hanchan+', related_name='user_hanchan+',
verbose_name=_('Player 2')) verbose_name=_('Player 2'))
player2_input_score = models.IntegerField(_('Score')) player2_input_score = models.IntegerField(_('Score'))
@@ -130,7 +132,7 @@ class Hanchan(models.Model):
player3 = models.ForeignKey( player3 = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
on_delete=models.CASCADE, on_delete=models.PROTECT,
related_name='user_hanchan+', related_name='user_hanchan+',
verbose_name=_('Player 3')) verbose_name=_('Player 3'))
player3_input_score = models.IntegerField(_('Score')) player3_input_score = models.IntegerField(_('Score'))
@@ -149,7 +151,7 @@ class Hanchan(models.Model):
player4 = models.ForeignKey( player4 = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
on_delete=models.CASCADE, on_delete=models.PROTECT,
related_name='user_hanchan+', related_name='user_hanchan+',
verbose_name=_('Player 4')) verbose_name=_('Player 4'))
player4_input_score = models.IntegerField(_('Score')) player4_input_score = models.IntegerField(_('Score'))
@@ -333,32 +335,49 @@ class KyuDanRanking(models.Model):
Im Gegensatz zum Ladder Ranking ist das nicht Saison gebunden. Im Gegensatz zum Ladder Ranking ist das nicht Saison gebunden.
Deswegen läuft es getrennt. Deswegen läuft es getrennt.
""" """
user = models.OneToOneField(settings.AUTH_USER_MODEL) user = models.OneToOneField(settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)
dan = models.PositiveSmallIntegerField(blank=True, null=True) dan = models.PositiveSmallIntegerField(blank=True, null=True)
dan_points = models.PositiveIntegerField(default=0) dan_points = models.PositiveIntegerField(default=0)
max_dan_points = models.PositiveIntegerField(default=0)
kyu = models.PositiveSmallIntegerField(default=10, blank=True, null=True) kyu = models.PositiveSmallIntegerField(default=10, blank=True, null=True)
kyu_points = models.PositiveIntegerField(default=0) kyu_points = models.PositiveIntegerField(default=0)
won_hanchans = models.PositiveIntegerField(default=0) won_hanchans = models.PositiveIntegerField(default=0)
good_hanchans = models.PositiveIntegerField(default=0) good_hanchans = models.PositiveIntegerField(default=0)
hanchan_count = models.PositiveIntegerField(default=0) hanchan_count = models.PositiveIntegerField(default=0)
legacy_date = models.DateField(blank=True, null=True) legacy_date = models.DateField(blank=True, null=True)
legacy_hanchan_count = models.PositiveIntegerField(default=0) legacy_dan = models.PositiveSmallIntegerField(blank=True, null=True)
legacy_dan_points = models.PositiveIntegerField(default=0) legacy_dan_points = models.PositiveIntegerField(blank=True, null=True)
legacy_kyu_points = models.PositiveIntegerField(default=0) legacy_kyu = models.PositiveSmallIntegerField(blank=True, null=True)
wins_in_a_row = 0 legacy_kyu_points = models.PositiveIntegerField(blank=True, null=True)
legacy_hanchan_count = models.PositiveIntegerField(blank=True, null=True)
legacy_good_hanchans = models.PositiveIntegerField(blank=True, null=True)
legacy_won_hanchans = models.PositiveIntegerField(blank=True, null=True)
wins_in_a_row = models.PositiveIntegerField(default=0)
last_hanchan_date = models.DateTimeField(blank=True, null=True)
objects = managers.KyuDanRankingManager() 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')
verbose_name_plural = _(u'Kyū/Dan Rankings') verbose_name_plural = _(u'Kyū/Dan Rankings')
def __unicode__(self): @property
if self.dan_points is not None: 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) return u"%s - %d. Dan" % (self.user.username, self.dan or 1)
else: 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): def append_3_in_a_row_bonuspoints(self, hanchan):
u""" u"""
@@ -366,12 +385,14 @@ class KyuDanRanking(models.Model):
das er einen Dan Rang aufsteigt. Dies wird als Kommentar abgespeichert, das er einen Dan Rang aufsteigt. Dies wird als Kommentar abgespeichert,
um es besser nachvollziehen zu können. um es besser nachvollziehen zu können.
""" """
if self.dan and hanchan.placement == 1: if not self.dan or not settings.DAN_3_WINS_IN_A_ROW:
return
if hanchan.placement == 1:
self.wins_in_a_row += 1 self.wins_in_a_row += 1
else: else:
self.wins_in_a_row = 0 self.wins_in_a_row = 0
return
if self.dan and self.wins_in_a_row >= 3 and self.dan < 9: if self.wins_in_a_row >= 3 and self.dan < 9:
LOGGER.info( LOGGER.info(
'adding bonuspoints for 3 wins in a row for %s', self.user) 'adding bonuspoints for 3 wins in a row for %s', self.user)
new_dan_rank = self.dan + 1 new_dan_rank = self.dan + 1
@@ -392,8 +413,8 @@ class KyuDanRanking(models.Model):
bonus_points, new_dan_rank) bonus_points, new_dan_rank)
self.dan_points += bonus_points self.dan_points += bonus_points
self.wins_in_a_row = 0 self.wins_in_a_row = 0
self.update_rank()
# TODO: Komplett Überabreiten!
def append_tournament_bonuspoints(self, hanchan): def append_tournament_bonuspoints(self, hanchan):
""" """
Prüft ob es die letzte Hanchan in einem Turnier war. Wenn ja werden Prüft ob es die letzte Hanchan in einem Turnier war. Wenn ja werden
@@ -406,29 +427,29 @@ class KyuDanRanking(models.Model):
user=self.user, event=hanchan.event user=self.user, event=hanchan.event
).order_by('-start') ).order_by('-start')
last_hanchan_this_event = hanchans_this_event[0] last_hanchan_this_event = hanchans_this_event[0]
if hanchan != last_hanchan_this_event: # Das braucht nur am Ende eines Turnieres gemacht werden.
# Das braucht nur am Ende eines Turnieres gemacht werden. if hanchan != last_hanchan_this_event: return False
return False event_ranking = EventRanking.objects.get(
else: user=self.user,
event_ranking = EventRanking.objects.get( event=hanchan.event
user=self.user, )
event=hanchan.event if event_ranking.placement == 1:
) bonus_points += settings.TOURNAMENT_WIN_BONUSPOINTS
if event_ranking.placement == 1: hanchan.player_comment += u'+{0:d} Punkte Turnier gewonnen. '.format(
bonus_points += 4 settings.TOURNAMENT_WIN_BONUSPOINTS)
hanchan.player_comment += u'+4 Punkte Turnier gewonnen. ' if event_ranking.avg_placement == 1:
if event_ranking.avg_placement == 1: bonus_points += settings.TOURNAMENT_FLAWLESS_VICTORY_BONUSPOINTS
bonus_points += 8 hanchan.player_comment += u'+{0:d} Pkt: alle Spiele des Turnieres gewonnen. '.format(
hanchan.player_comment += u'+8 Pkt: alle Spiele des Turnieres gewonnen. ' settings.TOURNAMENT_FLAWLESS_VICTORY_BONUSPOINTS)
if bonus_points and self.dan: if bonus_points and self.dan:
hanchan.dan_points += bonus_points hanchan.dan_points += bonus_points
self.dan_points += bonus_points self.dan_points += bonus_points
elif bonus_points: elif bonus_points:
hanchan.kyu_points += bonus_points hanchan.kyu_points += bonus_points
self.kyu_points += bonus_points self.kyu_points += bonus_points
hanchan.bonus_points += bonus_points hanchan.bonus_points += bonus_points
return True return True
def get_absolute_url(self): def get_absolute_url(self):
if self.dan or self.dan_points > 0: if self.dan or self.dan_points > 0:
@@ -436,62 +457,67 @@ 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.
""" """
self.dan = None valid_hanchans = Hanchan.objects.confirmed(user=self.user)
self.dan_points = self.legacy_dan_points or 0 valid_hanchans = valid_hanchans.order_by('start')
self.kyu = None if since and self.last_hanchan_date and since < self.last_hanchan_date:
self.kyu_points = self.legacy_kyu_points or 0 force_recalc = True
self.hanchan_count = self.legacy_hanchan_count or 0 if until and self.last_hanchan_date and until < self.last_hanchan_date:
self.good_hanchans = 0 force_recalc = True
self.won_hanchans = 0 if force_recalc:
self.update_rank() # Setze alles auf die legacy Werte und berechne alles von neuem.
self.dan = self.legacy_dan
self.dan_points = self.legacy_dan_points or 0
self.max_dan_points = self.dan_points
self.kyu = self.legacy_kyu
self.kyu_points = self.legacy_kyu_points or 0
self.hanchan_count = self.legacy_hanchan_count or 0
self.good_hanchans = self.legacy_good_hanchans or 0
self.won_hanchans = self.legacy_won_hanchans or 0
self.last_hanchan_date = None
self.update_rank()
since = timezone.make_aware(datetime.combine(
self.legacy_date,
time(23, 59, 59))) if self.legacy_date else None
elif self.last_hanchan_date:
since = self.last_hanchan_date
elif self.legacy_date:
since = timezone.make_aware(
datetime.combine(self.legacy_date, time(0, 0, 0))
)
LOGGER.info( LOGGER.info(
"recalculating Kyu/Dan points for %s since %s...", "recalculating Kyu/Dan points for %(user)s since %(since)s...",
self.user, str(hanchan_start) {'user': self.user, 'since': str(since)}
) )
valid_hanchans = Hanchan.objects.confirmed_hanchans( if since:
user=self.user).order_by('start') valid_hanchans = valid_hanchans.filter(start__gt=since)
if self.legacy_date: if until:
valid_hanchans = valid_hanchans.filter(start__gt=self.legacy_date) valid_hanchans = valid_hanchans.filter(start__lte=until)
""" 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: for hanchan in valid_hanchans:
self.hanchan_count += 1
hanchan.get_playerdata(self.user) hanchan.get_playerdata(self.user)
if hanchan_start and hanchan_start < hanchan.start: if since and hanchan.start < since:
LOGGER.debug(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()
else: else:
hanchan.bonus_points = 0 hanchan.bonus_points = 0
hanchan.player_comment = u"" hanchan.player_comment = ""
self.update_hanchan_points(hanchan) self.update_hanchan_points(hanchan)
if hanchan.event.mahjong_tournament: if hanchan.event.mahjong_tournament:
self.append_tournament_bonuspoints(hanchan) self.append_tournament_bonuspoints(hanchan)
self.update_rank() self.update_rank()
self.append_3_in_a_row_bonuspoints(hanchan) self.append_3_in_a_row_bonuspoints(hanchan)
self.update_rank()
hanchan.update_playerdata(self.user) hanchan.update_playerdata(self.user)
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
LOGGER.debug( self.last_hanchan_date = hanchan.start
'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) self.save(force_update=True)
def update_hanchan_points(self, hanchan): def update_hanchan_points(self, hanchan):
@@ -502,7 +528,7 @@ class KyuDanRanking(models.Model):
""" """
hanchan.kyu_points = None hanchan.kyu_points = None
hanchan.dan_points = None hanchan.dan_points = None
if hanchan.event.mahjong_tournament: if hanchan.event.mahjong_tournament and settings.TOURNAMENT_POINT_SYSTEM:
"""Für Turniere gelten andere Regeln zur Punktevergabe: """Für Turniere gelten andere Regeln zur Punktevergabe:
1. Platz 4 Punkte 1. Platz 4 Punkte
2. Platz 3 Punkte 2. Platz 3 Punkte
@@ -526,6 +552,7 @@ class KyuDanRanking(models.Model):
hanchan.dan_points = -1 hanchan.dan_points = -1
elif hanchan.placement == 4: elif hanchan.placement == 4:
hanchan.dan_points = -2 hanchan.dan_points = -2
# otherwise player must be in the kyu ranking
elif hanchan.game_score >= 60000: elif hanchan.game_score >= 60000:
hanchan.kyu_points = 3 hanchan.kyu_points = 3
elif hanchan.game_score >= 30000: elif hanchan.game_score >= 30000:
@@ -539,45 +566,44 @@ class KyuDanRanking(models.Model):
if self.dan: if self.dan:
# Only substract so much points that player has 0 Points: # Only substract so much points that player has 0 Points:
if self.dan_points + hanchan.dan_points < 0: if self.dan_points + hanchan.dan_points < 0:
hanchan.player_comment = 'Spieler unterschreitet 0 Punkte.' \
'(Original {} Punkte)'.format(hanchan.dan_points)
hanchan.dan_points -= (self.dan_points + hanchan.dan_points) hanchan.dan_points -= (self.dan_points + hanchan.dan_points)
self.dan_points += hanchan.dan_points self.dan_points += hanchan.dan_points
else: else:
# Only substract so much points that player has 0 Points: # Only substract so much points that player has 0 Points:
if self.kyu_points + hanchan.kyu_points < 0: if self.kyu_points + hanchan.kyu_points < 0:
hanchan.player_comment = 'Spieler unterschreitet 0 Punkte.' \
'(Original {} Punkte)'.format(hanchan.kyu_points)
hanchan.kyu_points -= (self.kyu_points + hanchan.kyu_points) 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
# aussagekräfig ist. Überarbeiten?
def update_rank(self): def update_rank(self):
if self.dan and self.dan_points < 0: # Update Dan ranking:
self.dan_points = 0 if self.dan or self.dan_points > 0:
self.dan = 1 if settings.DAN_ALLOW_DROP_DOWN:
elif self.dan or self.dan_points > 0: self.dan = max((dan for min_points, dan in settings.DAN_RANKS
old_dan = self.dan if self.dan_points > min_points))
for min_points, dan_rank in settings.DAN_RANKS: else:
if self.dan_points > min_points: self.max_dan_points = max(self.max_dan_points, self.dan_points)
self.dan = dan_rank self.dan = max((dan for min_points, dan in settings.DAN_RANKS
break if self.max_dan_points > min_points))
if old_dan is None or self.dan > old_dan:
self.wins_in_a_row = 0 # jump from Kyu to Dan
elif self.kyu_points < 1:
self.kyu_points = 0
self.kyu = 10
elif self.kyu_points > 50: elif self.kyu_points > 50:
self.dan = 1 self.dan = 1
self.kyu = None
self.dan_points = 0 self.dan_points = 0
self.kyu = None
self.kyu_points = 0 self.kyu_points = 0
self.wins_in_a_row = 0
# update Kyu ranking_
else: else:
for min_points, kyu_rank in settings.KYU_RANKS: self.kyu = min((kyu for min_points, kyu in settings.KYU_RANKS
if self.kyu_points > min_points: if self.kyu_points > min_points))
self.kyu = kyu_rank
break
class SeasonRanking(models.Model): class SeasonRanking(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
season = models.PositiveSmallIntegerField(_('Season')) season = models.PositiveSmallIntegerField(_('Season'))
placement = models.PositiveIntegerField(blank=True, null=True) placement = models.PositiveIntegerField(blank=True, null=True)
avg_placement = models.FloatField(blank=True, null=True) avg_placement = models.FloatField(blank=True, null=True)
@@ -593,9 +619,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

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

View File

@@ -40,3 +40,7 @@
{% endfor %} {% endfor %}
</table> </table>
{% endblock %} {% endblock %}
{% block buttonbar %}
<a href="?download=xlsx" class="button"><span class="fa fa-table"></span> Download</a>
{% endblock %}

View File

@@ -48,3 +48,7 @@
{% endfor %} {% endfor %}
</table> </table>
{% endblock %} {% endblock %}
{% block buttonbar %}
<a href="?download=xlsx" class="button"><span class="fa fa-table"></span> Download</a>
{% endblock %}

View File

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

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

@@ -7,15 +7,16 @@ from django.contrib import auth
from django.contrib.auth.mixins import LoginRequiredMixin, \ from django.contrib.auth.mixins import LoginRequiredMixin, \
PermissionRequiredMixin PermissionRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.core.urlresolvers import reverse from django.urls import reverse
from django.utils.translation import ugettext as _ from django.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 kasu import xlsx
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 +32,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 +50,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 +94,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 +108,32 @@ 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()
@@ -192,7 +157,6 @@ class PlayerScore(LoginRequiredMixin, generic.ListView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
user_model = auth.get_user_model() user_model = auth.get_user_model()
username = kwargs.get('username')
try: try:
self.user = user_model.objects.get( self.user = user_model.objects.get(
username=self.kwargs.get('username')) username=self.kwargs.get('username'))
@@ -200,6 +164,9 @@ class PlayerScore(LoginRequiredMixin, generic.ListView):
raise django.http.Http404( raise django.http.Http404(
_("No user found matching the name {}").format( _("No user found matching the name {}").format(
self.kwargs.get('username'))) self.kwargs.get('username')))
print(request.GET)
if request.GET.get('download') == 'xlsx':
return self.get_xlsx(request, *args, **kwargs)
return super(PlayerScore, self).get(request, *args, **kwargs) return super(PlayerScore, self).get(request, *args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@@ -218,26 +185,124 @@ class PlayerScore(LoginRequiredMixin, generic.ListView):
context['ladder_ranking'] = models.SeasonRanking(user=self.user) context['ladder_ranking'] = models.SeasonRanking(user=self.user)
return context return context
def get_xlsx(self, request, *args, **kwargs):
self.object_list = self.get_queryset()
response = django.http.HttpResponse(
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
response['Content-Disposition'] = 'attachment; ' \
'filename="{xlsx_filename}"'.format(
xlsx_filename=self.xlsx_filename)
xlxs_workbook = xlsx.Workbook()
xlxs_workbook.generate_sheet(
title=self.xlsx_filename.split('.')[0],
columns_settings=self.xlsx_columns,
object_list=self.object_list
)
xlxs_workbook.save(response)
return response
class PlayerDanScore(PlayerScore): 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) 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': 'player_comment',
'style': 'Content', 'width': 20, 'footer': 'Legacy Dan Punkte'},
)
@property
def xlsx_filename(self):
return "{username}_dan_score.xlsx".format(username=self.user.username)
class PlayerInvalidScore(PlayerScore): 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) self.xlsx_filename = "{username}_invalid_score.xlsx".format(
username=self.user.username)
return models.Hanchan.objects.unconfirmed(user=self.user)
class PlayerKyuScore(PlayerScore): class PlayerKyuScore(PlayerScore):
template_name = 'mahjong_ranking/player_kyu_score.html' template_name = 'mahjong_ranking/player_kyu_score.html'
def get_queryset(self): def get_queryset(self):
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): class PlayerLadderScore(PlayerScore):
@@ -259,3 +324,39 @@ class PlayerLadderScore(PlayerScore):
season=self.season season=self.season
) )
return hanchan_list return hanchan_list
@property
def xlsx_columns(self):
return (
{'col': 'A', 'title': 'Beginn', 'attr': 'start',
'style': 'Date Time', 'width': 14},
{'col': 'B', 'title': 'Termin', 'attr': 'event.name',
'style': 'Content', 'width': 16},
{'col': 'C', 'title': 'Platzierung', 'attr': 'placement',
'style': 'Integer', 'width': 11},
{'col': 'D', 'title': 'Spieler 1', 'attr': 'player1.username',
'style': 'Content', 'width': 16},
{'col': 'E', 'title': 'Punkte', 'attr': 'player1_game_score',
'style': 'Integer', 'width': 8},
{'col': 'F', 'title': 'Spieler 2', 'attr': 'player2.username',
'style': 'Content', 'width': 16},
{'col': 'G', 'title': 'Punkte', 'attr': 'player2_game_score',
'style': 'Integer', 'width': 8},
{'col': 'H', 'title': 'Spieler 3', 'attr': 'player3.username',
'style': 'Content', 'width': 16},
{'col': 'I', 'title': 'Punkte', 'attr': 'player3_game_score',
'style': 'Integer', 'width': 8},
{'col': 'J', 'title': 'Spieler 4', 'attr': 'player4.username',
'style': 'Content', 'width': 16},
{'col': 'K', 'title': 'Punkte', 'attr': 'player4_game_score',
'style': 'Integer', 'width': 8},
{'col': 'L', 'title': 'Punkte', 'attr': 'game_score',
'style': 'Integer', 'width': 8},
)
@property
def xlsx_filename(self):
return "{username}_ladder_score_{season}.xlsx".format(
username=self.user.username,
season=self.season
)

View File

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

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

View File

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

View File

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

View File

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

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

View File

@@ -7,7 +7,7 @@ from os import path
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.core.urlresolvers import reverse from django.urls import reverse
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@@ -80,6 +80,7 @@ class ActivationRequest(models.Model):
""" """
user = models.OneToOneField( user = models.OneToOneField(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
verbose_name=_('user') verbose_name=_('user')
) )
activation_key = models.CharField(_('activation key'), max_length=40) activation_key = models.CharField(_('activation key'), max_length=40)
@@ -215,6 +216,10 @@ class Membership(AbstractUser):
verbose_name = _('Membership') verbose_name = _('Membership')
verbose_name_plural = _('Memberships') verbose_name_plural = _('Memberships')
@property
def full_name(self):
return " ".join([self.last_name, self.first_name])
def __str__(self): def __str__(self):
return self.username return self.username

View File

@@ -35,7 +35,10 @@
<h3>Mahjong</h3> <h3>Mahjong</h3>
<ul> <ul>
{% if kyu_dan_ranking.dan %} {% if kyu_dan_ranking.dan %}
<li><strong>{{kyu_dan_ranking.dan}}. Dan: </strong> {{ kyu_dan_ranking.dan_points }} {% trans 'Points' %}</li> <li>
<strong>{{kyu_dan_ranking.dan}}. Dan: </strong> {{ kyu_dan_ranking.dan_points }} {% trans 'Points' %}
({% trans 'Maximum' %}: {{ kyu_dan_ranking.max_dan_points }})
</li>
{% elif kyu_dan_ranking.kyu%} {% elif kyu_dan_ranking.kyu%}
<li><strong>{{kyu_dan_ranking.kyu}}. Kyu: </strong> {{ kyu_dan_ranking.kyu_points }} {% trans 'Points' %}</li> <li><strong>{{kyu_dan_ranking.kyu}}. Kyu: </strong> {{ kyu_dan_ranking.kyu_points }} {% trans 'Points' %}</li>
{% endif %} {% endif %}

View File

@@ -7,7 +7,7 @@ from django import http
from django.conf import settings from django.conf import settings
from django.contrib import auth, messages from django.contrib import auth, messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.urlresolvers import reverse from django.urls import reverse
from django.http import Http404 from django.http import Http404
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _

View File

@@ -31,3 +31,15 @@ class CompressHtmlMiddleware(object):
response.content = strip_spaces_between_tags( response.content = strip_spaces_between_tags(
response.content).strip() response.content).strip()
return response return response
class SetRemoteAddrFromForwardedFor(object):
def process_request(self, request):
try:
real_ip = request.META['HTTP_X_FORWARDED_FOR']
real_ip = real_ip.split(",")[0]
except KeyError:
pass
else:
# HTTP_X_FORWARDED_FOR can be a comma-separated list of IPs.
# Take just the first one.
request.META['REMOTE_ADDR'] = real_ip