Compare commits
17 Commits
css3_redes
...
riichi_201
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b2e040fc9 | |||
| fdbf819092 | |||
| 9f6fffa4f4 | |||
| b7fab97715 | |||
| c030a31e2b | |||
| ade2a568f7 | |||
| c5781246fe | |||
| bb5081a78b | |||
| 854fd38740 | |||
| 6de1ecb102 | |||
| bf12060c3b | |||
| 5ad628f33a | |||
| 36272c60d6 | |||
| c428f6ed1f | |||
| 9276e97c36 | |||
| fd244f10e8 | |||
| 0a45cf1fd8 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -62,6 +62,7 @@ docs/_build/
|
||||
target/
|
||||
|
||||
#Django Development
|
||||
backup/
|
||||
/bower_components/
|
||||
/media/
|
||||
/node_modules/
|
||||
|
||||
63
TODO
63
TODO
@@ -8,15 +8,15 @@ src/utils/html_cleaner.py
|
||||
| | The code does not comply to PEP8.
|
||||
|----| | /srv/home/xeniac/Workspace/kasu/src/content/feeds.py
|
||||
| |++++| /srv/home/xeniac/Workspace/kasu/src/content/feeds.py
|
||||
| 7| 7|
|
||||
| 7| 7|
|
||||
| 8| 8| from content.models import Article
|
||||
| 9| 9|
|
||||
| 9| 9|
|
||||
| 10| |-MAX_ARTICLE_ITEMS = 10 # Maximum count of articles in the news RSS feed.
|
||||
| 11| |-MAX_COMMENT_ITEMS = 40 # Maximum count of comments in the comments RSS feed.
|
||||
| | 10|+MAX_ARTICLE_ITEMS = 10 # Maximum count of articles in the news RSS feed.
|
||||
| | 11|+MAX_COMMENT_ITEMS = 40 # Maximum count of comments in the comments RSS feed.
|
||||
| 12| 12|
|
||||
| 13| 13|
|
||||
| 12| 12|
|
||||
| 13| 13|
|
||||
| 14| 14| # Start ignoring PyLintBear (R0201)
|
||||
| | [NORMAL] PEP8Bear:
|
||||
| | The code does not comply to PEP8.
|
||||
@@ -25,7 +25,7 @@ src/utils/html_cleaner.py
|
||||
| 122| 122| def save(self, commit=True):
|
||||
| 123| 123| """ Create the new User, set him/her inactive, create an acitivation
|
||||
| 124| 124| request for the user and send him/her an activation email.
|
||||
| 125| |-
|
||||
| 125| |-
|
||||
| | 125|+
|
||||
| 126| 126| :param commit: commit the SQL and send the email if True
|
||||
| 127| 127| :return: the created User Object
|
||||
@@ -157,8 +157,8 @@ src/utils/middleware.py
|
||||
| |++++| /srv/home/xeniac/Workspace/kasu/src/mahjong_ranking/managers.py
|
||||
| 7| 7| class HanchanManager(models.Manager):
|
||||
| 8| 8| """
|
||||
| 9| 9| The ObjectManager for models.Hanchan QuerySets.
|
||||
| 10| |-
|
||||
| 9| 9| The ObjectManager for models.Hanchan QuerySets.
|
||||
| 10| |-
|
||||
| | 10|+
|
||||
| 11| 11| It adds many specific filters that makes many queries much easier.
|
||||
| 12| 12| """
|
||||
@@ -167,12 +167,12 @@ src/utils/middleware.py
|
||||
| | The code does not comply to PEP8.
|
||||
|----| | /srv/home/xeniac/Workspace/kasu/src/mahjong_ranking/managers.py
|
||||
| |++++| /srv/home/xeniac/Workspace/kasu/src/mahjong_ranking/managers.py
|
||||
| 14| 14|
|
||||
| 14| 14|
|
||||
| 15| 15| def confirmed_hanchans(self, user=None, **filter_args):
|
||||
| 16| 16| """ Return all valid and confirmed Hanchans.
|
||||
| 17| |-
|
||||
| 16| 16| """ Return all valid and confirmed Hanchans.
|
||||
| 17| |-
|
||||
| | 17|+
|
||||
| 18| 18| :param user: Only return Hanchans where this user participated.
|
||||
| 18| 18| :param user: Only return Hanchans where this user participated.
|
||||
| 19| 19| :param filter_args: To add specific arguments to the Django filter.
|
||||
| 20| 20| :return: QuerySet Object
|
||||
| | [NORMAL] PEP8Bear:
|
||||
@@ -185,7 +185,7 @@ src/utils/middleware.py
|
||||
| 42| |- [ hanchan.get_playerdata(user) for hanchan in queryset ]
|
||||
| | 42|+ [hanchan.get_playerdata(user) for hanchan in queryset]
|
||||
| 43| 43| return queryset
|
||||
| 44| 44|
|
||||
| 44| 44|
|
||||
| 45| 45| def kyu_hanchans(self, user, **filter_args):
|
||||
| | [NORMAL] PEP8Bear:
|
||||
| | The code does not comply to PEP8.
|
||||
@@ -197,7 +197,7 @@ src/utils/middleware.py
|
||||
| 60| |- [ hanchan.get_playerdata(user) for hanchan in queryset ]
|
||||
| | 60|+ [hanchan.get_playerdata(user) for hanchan in queryset]
|
||||
| 61| 61| return queryset
|
||||
| 62| 62|
|
||||
| 62| 62|
|
||||
| 63| 63| def season_hanchans(self, user=None, season=None):
|
||||
| | [NORMAL] PEP8Bear:
|
||||
| | The code does not comply to PEP8.
|
||||
@@ -209,7 +209,7 @@ src/utils/middleware.py
|
||||
| 90| |- [ hanchan.get_playerdata(user) for hanchan in queryset ]
|
||||
| | 90|+ [hanchan.get_playerdata(user) for hanchan in queryset]
|
||||
| 91| 91| return queryset
|
||||
| 92| 92|
|
||||
| 92| 92|
|
||||
| 93| 93| def unconfirmed_hanchans(self, user=None, **filter_args):
|
||||
|
||||
src/mahjong_ranking/managers.py
|
||||
@@ -417,7 +417,7 @@ src/content/models.py
|
||||
| | 60|+ user.registration_date.isoformat()
|
||||
| 61| 61| activation_key = hashlib.sha1(salt.encode()).hexdigest()
|
||||
| 62| 62| return self.create(user=user, activation_key=activation_key)
|
||||
| 63| 63|
|
||||
| 63| 63|
|
||||
|
||||
src/membership/models.py
|
||||
| 229| ········:param·args:·passed·through·the·save()·method·from·django··
|
||||
@@ -534,20 +534,20 @@ src/events/models.py
|
||||
| |++++| /srv/home/xeniac/Workspace/kasu/src/kasu/wsgi.py
|
||||
| 14| 14| if SOURCE_PATH not in sys.path:
|
||||
| 15| 15| sys.path.append(SOURCE_PATH)
|
||||
| 16| 16|
|
||||
| 16| 16|
|
||||
| 17| |-from django.core.wsgi import get_wsgi_application # Ignore PyLintBear (C0413) # Ignore PyLintBear (C0413)
|
||||
| | 17|+# Ignore PyLintBear (C0413) # Ignore PyLintBear (C0413)
|
||||
| | 18|+from django.core.wsgi import get_wsgi_application
|
||||
| 18| 19|
|
||||
| 18| 19|
|
||||
| 19| 20| os.environ['DJANGO_SETTINGS_MODULE'] = 'kasu.settings'
|
||||
| 20| 21|
|
||||
| 20| 21|
|
||||
| | [NORMAL] PEP8Bear:
|
||||
| | The code does not comply to PEP8.
|
||||
|----| | /srv/home/xeniac/Workspace/kasu/src/kasu/wsgi.py
|
||||
| |++++| /srv/home/xeniac/Workspace/kasu/src/kasu/wsgi.py
|
||||
| 18| 18|
|
||||
| 18| 18|
|
||||
| 19| 19| os.environ['DJANGO_SETTINGS_MODULE'] = 'kasu.settings'
|
||||
| 20| 20|
|
||||
| 20| 20|
|
||||
| 21| |-application = get_wsgi_application() # Ignore PyLintBear (C0103) # Ignore PyLintBear (C0103)
|
||||
| | 21|+# Ignore PyLintBear (C0103) # Ignore PyLintBear (C0103)
|
||||
| | 22|+application = get_wsgi_application()
|
||||
@@ -570,24 +570,24 @@ src/kasu/wsgi.py
|
||||
| | The code does not comply to PEP8.
|
||||
|----| | /srv/home/xeniac/Workspace/kasu/src/content/views.py
|
||||
| |++++| /srv/home/xeniac/Workspace/kasu/src/content/views.py
|
||||
| 197| 197|
|
||||
| 197| 197|
|
||||
| 198| 198| def get_object(self, queryset=None):
|
||||
| 199| 199| """ Get the path from the URL and fetch the corresponding page.
|
||||
| 200| |-
|
||||
| 200| |-
|
||||
| | 200|+
|
||||
| 201| 201| First get the path wihout fileextentsion leading or trailing slashes,
|
||||
| 202| 202| then search in the database if such a page exists.
|
||||
| 203| 203|
|
||||
| 203| 203|
|
||||
| | [NORMAL] PEP8Bear:
|
||||
| | The code does not comply to PEP8.
|
||||
|----| | /srv/home/xeniac/Workspace/kasu/src/content/views.py
|
||||
| |++++| /srv/home/xeniac/Workspace/kasu/src/content/views.py
|
||||
| 288| 288|
|
||||
| 288| 288|
|
||||
| 289| 289| def get_context_data(self):
|
||||
| 290| 290| """ Adds recent ariticles and recent comments to the context.
|
||||
| 291| |-
|
||||
| 291| |-
|
||||
| | 291|+
|
||||
| 292| 292| :return: array() with the context data
|
||||
| 292| 292| :return: array() with the context data
|
||||
| 293| 293| """
|
||||
| 294| 294| page = models.Page.objects.get(slug='index')
|
||||
|
||||
@@ -766,11 +766,6 @@ src/mahjong_ranking/models.py
|
||||
| | [NORMAL] PyLintBear (W0201):
|
||||
| | 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
|
||||
| 330| class·KyuDanRanking(models.Model):
|
||||
| | [INFO] PyLintBear (R0902):
|
||||
@@ -865,7 +860,7 @@ src/maistar_ranking/models.py
|
||||
| 47| |- )
|
||||
| | 47|+ )
|
||||
| 48| 48| )
|
||||
| 49| 49|
|
||||
| 49| 49|
|
||||
| 50| 50| def test_html_cleaner(self):
|
||||
|
||||
src/maistar_ranking/managers.py
|
||||
@@ -951,10 +946,10 @@ src/events/views.py
|
||||
| | The code does not comply to PEP8.
|
||||
|----| | /srv/home/xeniac/Workspace/kasu/src/utils/massmailer.py
|
||||
| |++++| /srv/home/xeniac/Workspace/kasu/src/utils/massmailer.py
|
||||
| 68| 68|
|
||||
| 68| 68|
|
||||
| 69| 69| def set_header(self, name, value):
|
||||
| 70| 70| """Add or modify an E-Mail Header to the Messages
|
||||
| 71| |-
|
||||
| 71| |-
|
||||
| | 71|+
|
||||
| 72| 72| :param name: The Header Name that should be added
|
||||
| 73| 73| :param value: THe Header Value that shoud be added or set
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
SSH_LOGIN="kasu@s21.wservices.ch"
|
||||
SYNC_ASSESTS="requirements"
|
||||
SYNC_ASSESTS="requirements static"
|
||||
SYNC_SOURCECODE="src"
|
||||
EXCLUDE_FILES="*.pyc"
|
||||
|
||||
@@ -19,5 +19,5 @@ rsync -r --copy-links --delete ${SYNC_SOURCECODE} ${SSH_LOGIN}:~/ --exclude 'src
|
||||
|
||||
echo "Rebuild and reload django..."
|
||||
ssh ${SSH_LOGIN} "rm src/kasu/settings/development.*"
|
||||
ssh ${SSH_LOGIN} "virtualenv/bin/python ~/src/manage.py collectstatic -l --noinput -v1"
|
||||
ssh ${SSH_LOGIN} "~/virtualenv/bin/python ~/src/manage.py collectstatic -l --noinput -v1"
|
||||
ssh ${SSH_LOGIN} "~/init/kasu restart"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
beautifulsoup4
|
||||
django
|
||||
django < 2.0
|
||||
django-appconf
|
||||
django-ckeditor
|
||||
django-contrib-comments
|
||||
@@ -18,4 +18,4 @@ pytz
|
||||
requests
|
||||
requests-oauthlib
|
||||
social-auth-app-django
|
||||
social-auth-core
|
||||
social-auth-core
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -44,7 +44,8 @@ class Migration(migrations.Migration):
|
||||
('date_modified', models.DateTimeField(
|
||||
auto_now=True, verbose_name='Bearbeitet')),
|
||||
('author', models.ForeignKey(
|
||||
verbose_name='Autor', to=settings.AUTH_USER_MODEL)),
|
||||
verbose_name='Autor', to=settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE))
|
||||
],
|
||||
options={
|
||||
'ordering': ('-date_created',),
|
||||
@@ -144,7 +145,8 @@ class Migration(migrations.Migration):
|
||||
model_name='article',
|
||||
name='category',
|
||||
field=models.ForeignKey(
|
||||
verbose_name='Kategorie', to='content.Category'),
|
||||
verbose_name='Kategorie', to='content.Category',
|
||||
on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='page',
|
||||
|
||||
91
src/content/migrations/0006_auto_20171115_0653.py
Normal file
91
src/content/migrations/0006_auto_20171115_0653.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
27
src/content/migrations/0007_auto_20171214_1215.py
Normal file
27
src/content/migrations/0007_auto_20171214_1215.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@@ -3,9 +3,9 @@ from ckeditor_uploader.fields import RichTextUploadingField
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import get_language, ugettext as _
|
||||
@@ -41,6 +41,14 @@ def get_upload_path(instance, filename):
|
||||
return "categories/%s.%s" % (instance.slug, extension)
|
||||
|
||||
|
||||
def get_localized(obj, attr):
|
||||
""" Return the localilzed field, or the fallback if the localized is empty.
|
||||
"""
|
||||
fallback = attr + '_de'
|
||||
localized = attr + '_' + get_language()[:2]
|
||||
return getattr(obj, localized) or getattr(obj, fallback)
|
||||
|
||||
|
||||
class ArticleManager(models.Manager):
|
||||
"""Adds some predifined querys and joins some tables for faster querys."""
|
||||
|
||||
@@ -69,11 +77,14 @@ class Article(models.Model):
|
||||
headline_en = models.CharField('Headline', max_length=255, blank=True)
|
||||
content_de = RichTextUploadingField(_('Content'))
|
||||
content_en = RichTextUploadingField('Content', blank=True)
|
||||
category = models.ForeignKey('Category', verbose_name=_('Category'))
|
||||
category = models.ForeignKey('Category',
|
||||
on_delete=models.PROTECT,
|
||||
verbose_name=_('Category'))
|
||||
image = models.ImageField(_('Image'), upload_to='news/',
|
||||
blank=True, null=True)
|
||||
slug = models.SlugField(_('Slug'), unique_for_month='date_created')
|
||||
author = models.ForeignKey(settings.AUTH_USER_MODEL,
|
||||
on_delete=models.PROTECT,
|
||||
verbose_name=_('Author'))
|
||||
status = models.SmallIntegerField(_('Status'), choices=STATUS_CHOICES,
|
||||
default=STATUS_PUBLISHED)
|
||||
@@ -115,16 +126,12 @@ class Article(models.Model):
|
||||
@property
|
||||
def headline(self):
|
||||
"""Return the localized headline, fallback to german if necessary."""
|
||||
return mark_safe(
|
||||
getattr(self, "headline_%s" % get_language(), self.headline_de)
|
||||
)
|
||||
return mark_safe(get_localized(self, 'headline'))
|
||||
|
||||
@property
|
||||
def content(self):
|
||||
"""Return the localized content, fallback to german if necessary."""
|
||||
return mark_safe(
|
||||
getattr(self, "content_%s" % get_language(), self.content_de)
|
||||
)
|
||||
return mark_safe(get_localized(self, 'content'))
|
||||
|
||||
|
||||
class Category(models.Model):
|
||||
@@ -146,13 +153,12 @@ class Category(models.Model):
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the localized name, fallback to german if necessary."""
|
||||
return getattr(self, "name_%s" % get_language(), self.name_de)
|
||||
return get_localized(self, 'name')
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
"""Return the localized description, fallback to german if necessary."""
|
||||
return getattr(self, "description_%s" % get_language(),
|
||||
self.description_de)
|
||||
return get_localized(self, 'description')
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""Return the URL of the article archive, filtered on this category."""
|
||||
@@ -261,9 +267,7 @@ class Page(models.Model):
|
||||
@property
|
||||
def content(self):
|
||||
"""Return the localized content, fallback to german if necessary."""
|
||||
return mark_safe(
|
||||
getattr(self, "content_%s" % get_language(), self.content_de)
|
||||
)
|
||||
return mark_safe(get_localized(self, 'content'))
|
||||
|
||||
@property
|
||||
def css_class(self):
|
||||
@@ -275,23 +279,22 @@ class Page(models.Model):
|
||||
@property
|
||||
def description(self):
|
||||
"""Return the localized description, fallback to german if necessary."""
|
||||
return getattr(self, "description_%s" % get_language(),
|
||||
self.description_de)
|
||||
return get_localized(self, 'description')
|
||||
|
||||
@property
|
||||
def menu_name(self):
|
||||
"""Return the localized menu name, fallback to german if necessary."""
|
||||
return getattr(self, "menu_name_%s" % get_language(), self.menu_name_de)
|
||||
return get_localized(self, 'menu_name')
|
||||
|
||||
@property
|
||||
def pdf_file(self):
|
||||
"""Return the localized PDF file, fallback to german if necessary."""
|
||||
return getattr(self, "pdf_%s" % get_language(), self.pdf_de)
|
||||
return get_localized(self, 'pdf_file')
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
"""Return the localized title, fallback to german if necessary."""
|
||||
return getattr(self, "title_%s" % get_language(), self.title_de)
|
||||
return get_localized(self, 'title')
|
||||
|
||||
def clean(self):
|
||||
"""set the URL path, the right content type, and scrub the HTML code."""
|
||||
|
||||
@@ -31,6 +31,10 @@ class EventManager(models.Manager):
|
||||
"""Returns all past events."""
|
||||
return self.filter(start__lt=now())
|
||||
|
||||
def latest(self, limit=None):
|
||||
result = self.filter(start__lt=now()).order_by('-start', '-end')
|
||||
return result[0:limit] if limit else result
|
||||
|
||||
def upcoming(self, limit=None):
|
||||
"""Returns the next 'limit' upcoming events.
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import events.models
|
||||
import django.db.models.deletion
|
||||
from django.db import models, migrations
|
||||
|
||||
import events.models
|
||||
import utils
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
@@ -17,7 +17,8 @@ class Migration(migrations.Migration):
|
||||
name='Event',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID',
|
||||
serialize=False, auto_created=True, primary_key=True)),
|
||||
serialize=False, auto_created=True,
|
||||
primary_key=True)),
|
||||
('name', models.CharField(max_length=255, verbose_name='Name')),
|
||||
('description', models.TextField(
|
||||
verbose_name='Beschreibung', blank=True)),
|
||||
@@ -26,13 +27,20 @@ class Migration(migrations.Migration):
|
||||
null=True, verbose_name='Ende', blank=True)),
|
||||
('url', models.URLField(verbose_name='Homepage', blank=True)),
|
||||
('image', models.ImageField(storage=utils.OverwriteStorage(
|
||||
), upload_to=events.models.get_upload_path, null=True, verbose_name='Bild', blank=True)),
|
||||
), upload_to=events.models.get_upload_path, null=True,
|
||||
verbose_name='Bild', blank=True)),
|
||||
('is_tournament', models.BooleanField(default=False,
|
||||
help_text='Diese Veranstaltung ist ein Turnier, es gelten andere Regeln f\xfcr das Kyu Ranking.', verbose_name='Turnier')),
|
||||
help_text='Diese Veranstaltung ist ein Turnier, es gelten andere Regeln f\xfcr das Kyu Ranking.',
|
||||
verbose_name='Turnier')),
|
||||
('photo_count', models.PositiveIntegerField(
|
||||
default=0, editable=False)),
|
||||
('event_series', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, editable=False, to='events.Event', blank=True,
|
||||
help_text='Wenn dieser Termin zu einer Veranstaltungsreihe geh\xf6rt werden Ort, Beschreibung, Bild und Homepage von dem hier angegebenen Event \xfcbernommen.', null=True, verbose_name='Veranstaltungsreihen')),
|
||||
('event_series',
|
||||
models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL,
|
||||
editable=False, to='events.Event',
|
||||
blank=True,
|
||||
help_text='Wenn dieser Termin zu einer Veranstaltungsreihe geh\xf6rt werden Ort, Beschreibung, Bild und Homepage von dem hier angegebenen Event \xfcbernommen.',
|
||||
null=True,
|
||||
verbose_name='Veranstaltungsreihen')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('-start', '-end'),
|
||||
@@ -44,20 +52,310 @@ class Migration(migrations.Migration):
|
||||
name='Location',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID',
|
||||
serialize=False, auto_created=True, primary_key=True)),
|
||||
serialize=False, auto_created=True,
|
||||
primary_key=True)),
|
||||
('name', models.CharField(max_length=200, verbose_name='Name')),
|
||||
('description', models.TextField(
|
||||
verbose_name='Beschreibung', blank=True)),
|
||||
('image', models.ImageField(storage=utils.OverwriteStorage(
|
||||
), upload_to=events.models.get_upload_path, null=True, verbose_name='Bild', blank=True)),
|
||||
), upload_to=events.models.get_upload_path, null=True,
|
||||
verbose_name='Bild', blank=True)),
|
||||
('url', models.URLField(verbose_name='Homepage', blank=True)),
|
||||
('postal_code', models.CharField(
|
||||
max_length=6, verbose_name='Postleitzahl')),
|
||||
('street_address', models.CharField(
|
||||
max_length=127, verbose_name='Stra\xdfe')),
|
||||
('locality', models.CharField(max_length=127, verbose_name='Ort')),
|
||||
('country', models.CharField(max_length=2, verbose_name='Land', choices=[(b'GB', 'Vereinigtes K\xf6nigreich'), (b'AF', 'Afghanistan'), (b'AX', 'Aland Islands'), (b'AL', 'Albanien'), (b'DZ', 'Algerien'), (b'AS', 'Amerikanisch-Samoa'), (b'AD', 'Andorra'), (b'AO', 'Angola'), (b'AI', 'Anguilla'), (b'AQ', 'Antarktika'), (b'AG', 'Antigua und Barbuda'), (b'AR', 'Argentinien'), (b'AM', 'Armenien'), (b'AW', 'Aruba'), (b'AU', 'Australien'), (b'AT', '\xd6sterreich'), (b'AZ', 'Aserbaidschan'), (b'BS', 'Bahamas'), (b'BH', 'Bahrein'), (b'BD', 'Bangladesch'), (b'BB', 'Barbados'), (b'BY', 'Wei\xdfrussland'), (b'BE', 'Belgien'), (b'BZ', 'Belize'), (b'BJ', 'Benin'), (b'BM', 'Bermuda'), (b'BT', 'Bhutan'), (b'BO', 'Bolivien'), (b'BA', 'Bosnien und Herzegowina'), (b'BW', 'Botswana'), (b'BV', 'Bouvet Island'), (b'BR', 'Brasilien'), (b'IO', 'British Indian Ocean Territory'), (b'BN', 'Brunei Darussalam'), (b'BG', 'Bulgarien'), (b'BF', 'Burkina Faso'), (b'BI', 'Burundi'), (b'KH', 'Kambodscha'), (b'CM', 'Kamerun'), (b'CA', 'Kanada'), (b'CV', 'Cape Verde'), (b'KY', 'Cayman Islands'), (b'CF', 'Zentralafrikanische Republik'), (b'TD', 'Tschad'), (b'CL', 'Chile'), (b'CN', 'China'), (b'CX', 'Christmas Island'), (b'CC', 'Cocos (Keeling) Islands'), (b'CO', 'Kolumbien'), (b'KM', 'Komoren'), (b'CG', 'Kongo'), (b'CD', 'Kongo, Demokratische Republik'), (b'CK', 'Cook-Inseln'), (b'CR', 'Costa Rica'), (b'CI', "Cote d'Ivoire"), (b'HR', 'Kroatien'), (b'CU', 'Kuba'), (b'CY', 'Zypern'), (b'CZ', 'Tschechische Republik'), (b'DK', 'D\xe4nemark'), (b'DJ', 'Dschibuti'), (b'DM', 'Dominica'), (b'DO', 'Dominikanische Republik'), (b'EC', 'Ecuador'), (b'EG', '\xc4gypten'), (b'SV', 'El Salvador'), (b'GQ', '\xc4quatorial-Guinea'), (b'ER', 'Eritrea'), (b'EE', 'Estland'), (b'ET', '\xc4thiopien'), (b'FK', 'Falklandinseln (Malvinas)'), (b'FO', 'F\xe4r\xf6er-Inseln'), (b'FJ', 'Fidschi'), (b'FI', 'Finnland'), (b'FR', 'Frankreich'), (b'GF', 'Franz\xf6sisch-Guayana'), (b'PF', 'Franz\xf6sisch-Polynesien'), (b'TF', 'Franz\xf6sisch S\xfcdliche Territorien'), (b'GA', 'Gabun'), (b'GM', 'Gambia'), (b'GE', 'Georgia'), (b'DE', 'Deutschland'), (b'GH', 'Ghana'), (b'GI', 'Gibraltar'), (b'GR', 'Griechenland'), (b'GL', 'Gr\xf6nland'), (b'GD', 'Grenada'), (b'GP', 'Guadeloupe'), (b'GU', 'Guam'), (b'GT', 'Guatemala'), (b'GG', 'Guernsey'), (b'GN', 'Guinea'), (b'GW', 'Guinea-Bissau'), (b'GY', 'Guyana'), (b'HT', 'Haiti'), (b'HM', 'Heard und McDonald Inseln'), (b'VA', 'Heiliger Stuhl (Vatikanstadt)'), (b'HN', 'Honduras'), (b'HK', 'Hongkong'), (b'HU', 'Ungarn'), (b'IS', 'Island'), (b'IN', 'Indien'), (b'ID', 'Indonesien'), (b'IR', 'Iran, Islamische Republik'), (b'IQ', 'Irak'), (b'IE', 'Irland'), (b'IM', 'Isle of Man'), (b'IL', 'Israel'), (b'IT', 'Italien'), (b'JM', 'Jamaika'), (b'JP', 'Japan'), (b'JE', 'Jersey'), (b'JO', 'Jordan'), (b'KZ', 'Kasachstan'), (b'KE', 'Kenia'), (b'KI', 'Kiribati'), (b'KP', 'Korea, Demokratische Volksrepublik'), (b'KR', 'Korea, Republik'), (b'KW', 'Kuwait'), (b'KG', 'Kirgisistan'), (b'LA', 'Lao Demokratischen Volksrepublik'), (b'LV', 'Lettland'), (b'LB', 'Libanon'), (
|
||||
b'LS', 'Lesotho'), (b'LR', 'Liberia'), (b'LY', 'Libyen'), (b'LI', 'Liechtenstein'), (b'LT', 'Litauen'), (b'LU', 'Luxemburg'), (b'MO', 'Macao'), (b'MK', 'Mazedonien, die ehemalige jugoslawische Republik'), (b'MG', 'Madagaskar'), (b'MW', 'Malawi'), (b'MY', 'Malaysia'), (b'MV', 'Malediven'), (b'ML', 'Mali'), (b'MT', 'Malta'), (b'MH', 'Marshall Islands'), (b'MQ', 'Martinique'), (b'MR', 'Mauretanien'), (b'MU', 'Mauritius'), (b'YT', 'Mayotte'), (b'MX', 'Mexiko'), (b'FM', 'Mikronesien, F\xf6derierte Staaten von'), (b'MD', 'Moldawien'), (b'MC', 'Monaco'), (b'MN', 'Mongolei'), (b'ME', 'Montenegro'), (b'MS', 'Montserrat'), (b'MA', 'Marokko'), (b'MZ', 'Mosambik'), (b'MM', 'Myanmar'), (b'NA', 'Namibia'), (b'NR', 'Nauru'), (b'NP', 'Nepal'), (b'NL', 'Niederlande'), (b'AN', 'Niederl\xe4ndische Antillen'), (b'NC', 'Neukaledonien'), (b'NZ', 'New Zealand'), (b'NI', 'Nicaragua'), (b'NE', 'Niger'), (b'NG', 'Nigeria'), (b'NU', 'Niue'), (b'NF', 'Norfolk Island'), (b'MP', 'Northern Mariana Islands'), (b'NO', 'Norwegen'), (b'OM', 'Oman'), (b'PK', 'Pakistan'), (b'PW', 'Palau'), (b'PS', 'Pal\xe4stinensische Autonomiegebiete'), (b'PA', 'Panama'), (b'PG', 'Papua-Neuguinea'), (b'PY', 'Paraguay'), (b'PE', 'Peru'), (b'PH', 'Philippinen'), (b'PN', 'Pitcairn'), (b'PL', 'Polen'), (b'PT', 'Portugal'), (b'PR', 'Puerto Rico'), (b'QA', 'Katar'), (b'RE', 'Wiedervereinigung'), (b'RO', 'Rum\xe4nien'), (b'RU', 'Russischen F\xf6deration'), (b'RW', 'Ruanda'), (b'BL', 'Saint Barthelemy'), (b'SH', 'Saint Helena'), (b'KN', 'Saint Kitts und Nevis'), (b'LC', 'Santa Lucia'), (b'MF', 'Santa Martin'), (b'PM', 'Saint Pierre und Miquelon'), (b'VC', 'Saint Vincent und die Grenadinen'), (b'WS', 'Samoa'), (b'SM', 'San Marino'), (b'ST', 'Sao Tome und Principe'), (b'SA', 'Saudi-Arabien'), (b'SN', 'Senegal'), (b'RS', 'Serbien'), (b'SC', 'Seychellen'), (b'SL', 'Sierra Leone'), (b'SG', 'Singapur'), (b'SK', 'Slowakei'), (b'SI', 'Slowenien'), (b'SB', 'Salomon-Inseln'), (b'SO', 'Somalia'), (b'ZA', 'S\xfcdafrika'), (b'GS', 'S\xfcdgeorgien und die S\xfcdlichen Sandwichinseln'), (b'ES', 'Spanien'), (b'LK', 'Sri Lanka'), (b'SD', 'Sudan'), (b'SR', 'Suriname'), (b'SJ', 'Svalbard und Jan Mayen'), (b'SZ', 'Swaziland'), (b'SE', 'Schweden'), (b'CH', 'Schweiz'), (b'SY', 'Arabische Republik Syrien'), (b'TW', 'Taiwan, Province of China'), (b'TJ', 'Tadschikistan'), (b'TZ', 'Tansania, Vereinigte Republik'), (b'TH', 'Thailand'), (b'TL', 'Timor-Leste'), (b'TG', 'Togo'), (b'TK', 'Tokelau'), (b'TO', 'Tonga'), (b'TT', 'Trinidad und Tobago'), (b'TN', 'Tunesien'), (b'TR', 'T\xfcrkei'), (b'TM', 'Turkmenistan'), (b'TC', 'Turks-und Caicosinseln'), (b'TV', 'Tuvalu'), (b'UG', 'Uganda'), (b'UA', 'Ukraine'), (b'AE', 'Vereinigte Arabische Emirate'), (b'US', 'Vereinigte Staaten'), (b'UM', 'United States Minor Outlying Islands'), (b'UY', 'Uruguay'), (b'UZ', 'Usbekistan'), (b'VU', 'Vanuatu'), (b'VE', 'Venezuela'), (b'VN', 'Vietnam'), (b'VG', 'Virgin Islands, British'), (b'VI', 'Virgin Islands, US'), (b'WF', 'Wallis und Futuna'), (b'EH', 'Westsahara'), (b'YE', 'Jemen'), (b'ZM', 'Sambia'), (b'ZW', 'Zimbabwe')])),
|
||||
('locality',
|
||||
models.CharField(max_length=127, verbose_name='Ort')),
|
||||
('country', models.CharField(max_length=2, verbose_name='Land',
|
||||
choices=[(b'GB',
|
||||
'Vereinigtes K\xf6nigreich'),
|
||||
(b'AF', 'Afghanistan'),
|
||||
(b'AX', 'Aland Islands'),
|
||||
(b'AL', 'Albanien'),
|
||||
(b'DZ', 'Algerien'), (
|
||||
b'AS',
|
||||
'Amerikanisch-Samoa'),
|
||||
(b'AD', 'Andorra'),
|
||||
(b'AO', 'Angola'),
|
||||
(b'AI', 'Anguilla'),
|
||||
(b'AQ', 'Antarktika'), (
|
||||
b'AG',
|
||||
'Antigua und Barbuda'),
|
||||
(b'AR', 'Argentinien'),
|
||||
(b'AM', 'Armenien'),
|
||||
(b'AW', 'Aruba'),
|
||||
(b'AU', 'Australien'),
|
||||
(b'AT', '\xd6sterreich'),
|
||||
(b'AZ', 'Aserbaidschan'),
|
||||
(b'BS', 'Bahamas'),
|
||||
(b'BH', 'Bahrein'),
|
||||
(b'BD', 'Bangladesch'),
|
||||
(b'BB', 'Barbados'), (
|
||||
b'BY',
|
||||
'Wei\xdfrussland'),
|
||||
(b'BE', 'Belgien'),
|
||||
(b'BZ', 'Belize'),
|
||||
(b'BJ', 'Benin'),
|
||||
(b'BM', 'Bermuda'),
|
||||
(b'BT', 'Bhutan'),
|
||||
(b'BO', 'Bolivien'), (
|
||||
b'BA',
|
||||
'Bosnien und Herzegowina'),
|
||||
(b'BW', 'Botswana'),
|
||||
(b'BV', 'Bouvet Island'),
|
||||
(b'BR', 'Brasilien'), (
|
||||
b'IO',
|
||||
'British Indian Ocean Territory'),
|
||||
(b'BN',
|
||||
'Brunei Darussalam'),
|
||||
(b'BG', 'Bulgarien'),
|
||||
(b'BF', 'Burkina Faso'),
|
||||
(b'BI', 'Burundi'),
|
||||
(b'KH', 'Kambodscha'),
|
||||
(b'CM', 'Kamerun'),
|
||||
(b'CA', 'Kanada'),
|
||||
(b'CV', 'Cape Verde'),
|
||||
(b'KY', 'Cayman Islands'),
|
||||
(b'CF',
|
||||
'Zentralafrikanische Republik'),
|
||||
(b'TD', 'Tschad'),
|
||||
(b'CL', 'Chile'),
|
||||
(b'CN', 'China'), (b'CX',
|
||||
'Christmas Island'),
|
||||
(b'CC',
|
||||
'Cocos (Keeling) Islands'),
|
||||
(b'CO', 'Kolumbien'),
|
||||
(b'KM', 'Komoren'),
|
||||
(b'CG', 'Kongo'), (b'CD',
|
||||
'Kongo, Demokratische Republik'),
|
||||
(b'CK', 'Cook-Inseln'),
|
||||
(b'CR', 'Costa Rica'),
|
||||
(b'CI', "Cote d'Ivoire"),
|
||||
(b'HR', 'Kroatien'),
|
||||
(b'CU', 'Kuba'),
|
||||
(b'CY', 'Zypern'), (b'CZ',
|
||||
'Tschechische Republik'),
|
||||
(b'DK', 'D\xe4nemark'),
|
||||
(b'DJ', 'Dschibuti'),
|
||||
(b'DM', 'Dominica'), (
|
||||
b'DO',
|
||||
'Dominikanische Republik'),
|
||||
(b'EC', 'Ecuador'),
|
||||
(b'EG', '\xc4gypten'),
|
||||
(b'SV', 'El Salvador'), (
|
||||
b'GQ',
|
||||
'\xc4quatorial-Guinea'),
|
||||
(b'ER', 'Eritrea'),
|
||||
(b'EE', 'Estland'),
|
||||
(b'ET', '\xc4thiopien'), (
|
||||
b'FK',
|
||||
'Falklandinseln (Malvinas)'),
|
||||
(b'FO',
|
||||
'F\xe4r\xf6er-Inseln'),
|
||||
(b'FJ', 'Fidschi'),
|
||||
(b'FI', 'Finnland'),
|
||||
(b'FR', 'Frankreich'), (
|
||||
b'GF',
|
||||
'Franz\xf6sisch-Guayana'),
|
||||
(b'PF',
|
||||
'Franz\xf6sisch-Polynesien'),
|
||||
(b'TF',
|
||||
'Franz\xf6sisch S\xfcdliche Territorien'),
|
||||
(b'GA', 'Gabun'),
|
||||
(b'GM', 'Gambia'),
|
||||
(b'GE', 'Georgia'),
|
||||
(b'DE', 'Deutschland'),
|
||||
(b'GH', 'Ghana'),
|
||||
(b'GI', 'Gibraltar'),
|
||||
(b'GR', 'Griechenland'),
|
||||
(b'GL', 'Gr\xf6nland'),
|
||||
(b'GD', 'Grenada'),
|
||||
(b'GP', 'Guadeloupe'),
|
||||
(b'GU', 'Guam'),
|
||||
(b'GT', 'Guatemala'),
|
||||
(b'GG', 'Guernsey'),
|
||||
(b'GN', 'Guinea'),
|
||||
(b'GW', 'Guinea-Bissau'),
|
||||
(b'GY', 'Guyana'),
|
||||
(b'HT', 'Haiti'), (b'HM',
|
||||
'Heard und McDonald Inseln'),
|
||||
(b'VA',
|
||||
'Heiliger Stuhl (Vatikanstadt)'),
|
||||
(b'HN', 'Honduras'),
|
||||
(b'HK', 'Hongkong'),
|
||||
(b'HU', 'Ungarn'),
|
||||
(b'IS', 'Island'),
|
||||
(b'IN', 'Indien'),
|
||||
(b'ID', 'Indonesien'), (
|
||||
b'IR',
|
||||
'Iran, Islamische Republik'),
|
||||
(b'IQ', 'Irak'),
|
||||
(b'IE', 'Irland'),
|
||||
(b'IM', 'Isle of Man'),
|
||||
(b'IL', 'Israel'),
|
||||
(b'IT', 'Italien'),
|
||||
(b'JM', 'Jamaika'),
|
||||
(b'JP', 'Japan'),
|
||||
(b'JE', 'Jersey'),
|
||||
(b'JO', 'Jordan'),
|
||||
(b'KZ', 'Kasachstan'),
|
||||
(b'KE', 'Kenia'),
|
||||
(b'KI', 'Kiribati'), (
|
||||
b'KP',
|
||||
'Korea, Demokratische Volksrepublik'),
|
||||
(
|
||||
b'KR',
|
||||
'Korea, Republik'),
|
||||
(b'KW', 'Kuwait'),
|
||||
(b'KG', 'Kirgisistan'), (
|
||||
b'LA',
|
||||
'Lao Demokratischen Volksrepublik'),
|
||||
(b'LV', 'Lettland'),
|
||||
(b'LB', 'Libanon'), (
|
||||
b'LS', 'Lesotho'),
|
||||
(b'LR', 'Liberia'),
|
||||
(b'LY', 'Libyen'),
|
||||
(b'LI', 'Liechtenstein'),
|
||||
(b'LT', 'Litauen'),
|
||||
(b'LU', 'Luxemburg'),
|
||||
(b'MO', 'Macao'), (b'MK',
|
||||
'Mazedonien, die ehemalige jugoslawische Republik'),
|
||||
(b'MG', 'Madagaskar'),
|
||||
(b'MW', 'Malawi'),
|
||||
(b'MY', 'Malaysia'),
|
||||
(b'MV', 'Malediven'),
|
||||
(b'ML', 'Mali'),
|
||||
(b'MT', 'Malta'), (b'MH',
|
||||
'Marshall Islands'),
|
||||
(b'MQ', 'Martinique'),
|
||||
(b'MR', 'Mauretanien'),
|
||||
(b'MU', 'Mauritius'),
|
||||
(b'YT', 'Mayotte'),
|
||||
(b'MX', 'Mexiko'), (b'FM',
|
||||
'Mikronesien, F\xf6derierte Staaten von'),
|
||||
(b'MD', 'Moldawien'),
|
||||
(b'MC', 'Monaco'),
|
||||
(b'MN', 'Mongolei'),
|
||||
(b'ME', 'Montenegro'),
|
||||
(b'MS', 'Montserrat'),
|
||||
(b'MA', 'Marokko'),
|
||||
(b'MZ', 'Mosambik'),
|
||||
(b'MM', 'Myanmar'),
|
||||
(b'NA', 'Namibia'),
|
||||
(b'NR', 'Nauru'),
|
||||
(b'NP', 'Nepal'),
|
||||
(b'NL', 'Niederlande'), (
|
||||
b'AN',
|
||||
'Niederl\xe4ndische Antillen'),
|
||||
(b'NC', 'Neukaledonien'),
|
||||
(b'NZ', 'New Zealand'),
|
||||
(b'NI', 'Nicaragua'),
|
||||
(b'NE', 'Niger'),
|
||||
(b'NG', 'Nigeria'),
|
||||
(b'NU', 'Niue'),
|
||||
(b'NF', 'Norfolk Island'),
|
||||
(b'MP',
|
||||
'Northern Mariana Islands'),
|
||||
(b'NO', 'Norwegen'),
|
||||
(b'OM', 'Oman'),
|
||||
(b'PK', 'Pakistan'),
|
||||
(b'PW', 'Palau'), (b'PS',
|
||||
'Pal\xe4stinensische Autonomiegebiete'),
|
||||
(b'PA', 'Panama'), (
|
||||
b'PG',
|
||||
'Papua-Neuguinea'),
|
||||
(b'PY', 'Paraguay'),
|
||||
(b'PE', 'Peru'),
|
||||
(b'PH', 'Philippinen'),
|
||||
(b'PN', 'Pitcairn'),
|
||||
(b'PL', 'Polen'),
|
||||
(b'PT', 'Portugal'),
|
||||
(b'PR', 'Puerto Rico'),
|
||||
(b'QA', 'Katar'), (b'RE',
|
||||
'Wiedervereinigung'),
|
||||
(b'RO', 'Rum\xe4nien'), (
|
||||
b'RU',
|
||||
'Russischen F\xf6deration'),
|
||||
(b'RW', 'Ruanda'), (b'BL',
|
||||
'Saint Barthelemy'),
|
||||
(b'SH', 'Saint Helena'), (
|
||||
b'KN',
|
||||
'Saint Kitts und Nevis'),
|
||||
(b'LC', 'Santa Lucia'),
|
||||
(b'MF', 'Santa Martin'), (
|
||||
b'PM',
|
||||
'Saint Pierre und Miquelon'),
|
||||
(b'VC',
|
||||
'Saint Vincent und die Grenadinen'),
|
||||
(b'WS', 'Samoa'),
|
||||
(b'SM', 'San Marino'), (
|
||||
b'ST',
|
||||
'Sao Tome und Principe'),
|
||||
(b'SA', 'Saudi-Arabien'),
|
||||
(b'SN', 'Senegal'),
|
||||
(b'RS', 'Serbien'),
|
||||
(b'SC', 'Seychellen'),
|
||||
(b'SL', 'Sierra Leone'),
|
||||
(b'SG', 'Singapur'),
|
||||
(b'SK', 'Slowakei'),
|
||||
(b'SI', 'Slowenien'),
|
||||
(b'SB', 'Salomon-Inseln'),
|
||||
(b'SO', 'Somalia'),
|
||||
(b'ZA', 'S\xfcdafrika'), (
|
||||
b'GS',
|
||||
'S\xfcdgeorgien und die S\xfcdlichen Sandwichinseln'),
|
||||
(b'ES', 'Spanien'),
|
||||
(b'LK', 'Sri Lanka'),
|
||||
(b'SD', 'Sudan'),
|
||||
(b'SR', 'Suriname'), (
|
||||
b'SJ',
|
||||
'Svalbard und Jan Mayen'),
|
||||
(b'SZ', 'Swaziland'),
|
||||
(b'SE', 'Schweden'),
|
||||
(b'CH', 'Schweiz'), (
|
||||
b'SY',
|
||||
'Arabische Republik Syrien'),
|
||||
(b'TW',
|
||||
'Taiwan, Province of China'),
|
||||
(b'TJ', 'Tadschikistan'),
|
||||
(b'TZ',
|
||||
'Tansania, Vereinigte Republik'),
|
||||
(b'TH', 'Thailand'),
|
||||
(b'TL', 'Timor-Leste'),
|
||||
(b'TG', 'Togo'),
|
||||
(b'TK', 'Tokelau'),
|
||||
(b'TO', 'Tonga'), (b'TT',
|
||||
'Trinidad und Tobago'),
|
||||
(b'TN', 'Tunesien'),
|
||||
(b'TR', 'T\xfcrkei'),
|
||||
(b'TM', 'Turkmenistan'), (
|
||||
b'TC',
|
||||
'Turks-und Caicosinseln'),
|
||||
(b'TV', 'Tuvalu'),
|
||||
(b'UG', 'Uganda'),
|
||||
(b'UA', 'Ukraine'), (
|
||||
b'AE',
|
||||
'Vereinigte Arabische Emirate'),
|
||||
(b'US',
|
||||
'Vereinigte Staaten'), (
|
||||
b'UM',
|
||||
'United States Minor Outlying Islands'),
|
||||
(b'UY', 'Uruguay'),
|
||||
(b'UZ', 'Usbekistan'),
|
||||
(b'VU', 'Vanuatu'),
|
||||
(b'VE', 'Venezuela'),
|
||||
(b'VN', 'Vietnam'), (
|
||||
b'VG',
|
||||
'Virgin Islands, British'),
|
||||
(b'VI',
|
||||
'Virgin Islands, US'), (
|
||||
b'WF',
|
||||
'Wallis und Futuna'),
|
||||
(b'EH', 'Westsahara'),
|
||||
(b'YE', 'Jemen'),
|
||||
(b'ZM', 'Sambia'),
|
||||
(b'ZW', 'Zimbabwe')])),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Veranstaltungsort',
|
||||
@@ -67,6 +365,8 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='location',
|
||||
field=models.ForeignKey(to='events.Location'),
|
||||
field=models.ForeignKey(
|
||||
to='events.Location',
|
||||
on_delete=models.CASCADE),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import ckeditor.fields
|
||||
import events.models
|
||||
import easy_thumbnails.fields
|
||||
import django.db.models.deletion
|
||||
import utils
|
||||
import easy_thumbnails.fields
|
||||
from django.conf import settings
|
||||
from django.db import models, migrations
|
||||
|
||||
import events.models
|
||||
import utils
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('events', '0003_auto_20150823_2232'),
|
||||
@@ -22,18 +22,24 @@ class Migration(migrations.Migration):
|
||||
name='Photo',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID',
|
||||
serialize=False, auto_created=True, primary_key=True)),
|
||||
serialize=False, auto_created=True,
|
||||
primary_key=True)),
|
||||
('name', models.CharField(max_length=100,
|
||||
verbose_name='Name', blank=True)),
|
||||
('image', easy_thumbnails.fields.ThumbnailerImageField(
|
||||
upload_to=events.models.get_upload_path, storage=utils.OverwriteStorage(), verbose_name='Bild')),
|
||||
upload_to=events.models.get_upload_path,
|
||||
storage=utils.OverwriteStorage(), verbose_name='Bild')),
|
||||
('description', models.TextField(max_length=300,
|
||||
verbose_name='Beschreibung', blank=True)),
|
||||
verbose_name='Beschreibung',
|
||||
blank=True)),
|
||||
('on_startpage', models.BooleanField(default=False,
|
||||
help_text='Display this Photo on the Startpage Teaser', verbose_name='Startpage')),
|
||||
('created_date', models.DateTimeField(verbose_name='Published on')),
|
||||
help_text='Display this Photo on the Startpage Teaser',
|
||||
verbose_name='Startpage')),
|
||||
('created_date',
|
||||
models.DateTimeField(verbose_name='Published on')),
|
||||
('views', models.PositiveIntegerField(default=0,
|
||||
verbose_name='Number of views', editable=False)),
|
||||
verbose_name='Number of views',
|
||||
editable=False)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['created_date'],
|
||||
@@ -46,7 +52,8 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterModelOptions(
|
||||
name='event',
|
||||
options={'ordering': (
|
||||
'start', 'end'), 'verbose_name': 'Termin', 'verbose_name_plural': 'Termine'},
|
||||
'start', 'end'), 'verbose_name': 'Termin',
|
||||
'verbose_name_plural': 'Termine'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
@@ -57,14 +64,19 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='event_series',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, blank=True, to='events.Event',
|
||||
help_text='Wenn dieser Termin zu einer Veranstaltungsreihe geh\xf6rt werden Ort, Beschreibung, Bild und Homepage von dem hier angegebenen Event \xfcbernommen.', null=True, verbose_name='Veranstaltungsreihen'),
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.SET_NULL, blank=True,
|
||||
to='events.Event',
|
||||
help_text='Wenn dieser Termin zu einer Veranstaltungsreihe geh\xf6rt werden Ort, Beschreibung, Bild und Homepage von dem hier angegebenen Event \xfcbernommen.',
|
||||
null=True, verbose_name='Veranstaltungsreihen'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='image',
|
||||
field=easy_thumbnails.fields.ThumbnailerImageField(storage=utils.OverwriteStorage(
|
||||
), upload_to=events.models.get_upload_path, null=True, verbose_name='Bild', blank=True),
|
||||
field=easy_thumbnails.fields.ThumbnailerImageField(
|
||||
storage=utils.OverwriteStorage(
|
||||
), upload_to=events.models.get_upload_path, null=True,
|
||||
verbose_name='Bild', blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='location',
|
||||
@@ -75,17 +87,21 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='location',
|
||||
name='image',
|
||||
field=easy_thumbnails.fields.ThumbnailerImageField(storage=utils.OverwriteStorage(
|
||||
), upload_to=events.models.get_upload_path, null=True, verbose_name='Bild', blank=True),
|
||||
field=easy_thumbnails.fields.ThumbnailerImageField(
|
||||
storage=utils.OverwriteStorage(
|
||||
), upload_to=events.models.get_upload_path, null=True,
|
||||
verbose_name='Bild', blank=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='photo',
|
||||
name='event',
|
||||
field=models.ForeignKey(to='events.Event'),
|
||||
field=models.ForeignKey(
|
||||
to='events.Event', on_delete=models.CASCADE),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='photo',
|
||||
name='photographer',
|
||||
field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
|
||||
field=models.ForeignKey(
|
||||
to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE),
|
||||
),
|
||||
]
|
||||
|
||||
20
src/events/migrations/0008_auto_20171115_0653.py
Normal file
20
src/events/migrations/0008_auto_20171115_0653.py
Normal file
File diff suppressed because one or more lines are too long
32
src/events/migrations/0009_auto_20171214_1215.py
Normal file
32
src/events/migrations/0009_auto_20171214_1215.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@@ -1,4 +1,6 @@
|
||||
"""Mixins for Events."""
|
||||
from django.http import Http404
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
@@ -9,7 +11,6 @@ class EventArchiveMixin(object):
|
||||
date_field = 'start'
|
||||
make_object_list = True
|
||||
model = models.Event
|
||||
ordering = ('start', 'end')
|
||||
paginate_by = 15
|
||||
template_name = 'events/event_archive.html'
|
||||
|
||||
@@ -40,3 +41,20 @@ class EventDetailMixin(object):
|
||||
elif hasattr(self, 'object') and hasattr(self.object, 'event'):
|
||||
context['event'] = self.object.event
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
"""set event attribute from the URL kwarg event and
|
||||
load all related objects from the set model.
|
||||
|
||||
:return: a django QuerySets
|
||||
"""
|
||||
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()
|
||||
|
||||
@@ -4,10 +4,10 @@ import os
|
||||
from ckeditor.fields import RichTextField
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext as _
|
||||
from easy_thumbnails.fields import ThumbnailerImageField
|
||||
@@ -51,7 +51,7 @@ class Event(models.Model):
|
||||
"""An Event that could be a tournament, a game session, or an convention."""
|
||||
name = models.CharField(_('Name'), max_length=255)
|
||||
description = RichTextField(_("Description"), blank=True)
|
||||
location = models.ForeignKey('Location')
|
||||
location = models.ForeignKey('Location', on_delete=models.PROTECT)
|
||||
start = models.DateTimeField(_('Start'))
|
||||
end = models.DateTimeField(_('End'), blank=True, null=True)
|
||||
url = models.URLField(_('Homepage'), blank=True)
|
||||
@@ -220,13 +220,14 @@ class Photo(models.Model):
|
||||
upload_to=get_upload_path,
|
||||
storage=OverwriteStorage()
|
||||
)
|
||||
event = models.ForeignKey('events.Event')
|
||||
event = models.ForeignKey('events.Event', on_delete=models.PROTECT, )
|
||||
description = models.TextField(
|
||||
_("Description"),
|
||||
max_length=300,
|
||||
blank=True
|
||||
)
|
||||
photographer = models.ForeignKey(settings.AUTH_USER_MODEL)
|
||||
photographer = models.ForeignKey(settings.AUTH_USER_MODEL,
|
||||
on_delete=models.PROTECT)
|
||||
on_startpage = models.BooleanField(
|
||||
_("Startpage"),
|
||||
default=False,
|
||||
|
||||
@@ -3,7 +3,7 @@ from datetime import timedelta
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.urls import reverse
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.shortcuts import redirect
|
||||
@@ -30,7 +30,6 @@ class DeleteEventPhoto(PermissionRequiredMixin, mixins.EventDetailMixin,
|
||||
class EventArchiveIndex(mixins.EventArchiveMixin, generic.ArchiveIndexView):
|
||||
"""Index of the event archive, displays the upcoming events first."""
|
||||
allow_empty = True
|
||||
ordering = ('-start', '-end')
|
||||
|
||||
|
||||
class EventArchiveMonth(mixins.EventArchiveMixin, generic.MonthArchiveView):
|
||||
@@ -73,7 +72,7 @@ class EventForm(PermissionRequiredMixin, mixins.EventDetailMixin,
|
||||
if self.kwargs.get('pk') else models.Event()
|
||||
|
||||
|
||||
class EventGallery(mixins.EventDetailMixin, generic.ListView):
|
||||
class EventGallery(generic.ListView):
|
||||
"""Display a overview of all event photo albums."""
|
||||
template_name = 'events/photo_gallery.html'
|
||||
queryset = models.Event.objects.filter(
|
||||
|
||||
@@ -80,6 +80,7 @@ MIDDLEWARE_CLASSES = [
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'utils.middleware.SetRemoteAddrFromForwardedFor',
|
||||
'mahjong_ranking.middleware.DenormalizationUpdateMiddleware',
|
||||
]
|
||||
|
||||
@@ -229,7 +230,7 @@ LOGGING = {
|
||||
'loggers': {
|
||||
'django': {
|
||||
'handlers': ['console'],
|
||||
'level': 'INFO',
|
||||
'level': 'DEBUG',
|
||||
'propagate': True,
|
||||
},
|
||||
'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 = (
|
||||
(80, 9),
|
||||
(70, 8),
|
||||
@@ -255,7 +272,7 @@ DAN_RANKS = (
|
||||
(30, 4),
|
||||
(20, 3),
|
||||
(10, 2),
|
||||
(0, 1),
|
||||
(-1, 1),
|
||||
)
|
||||
|
||||
KYU_RANKS = (
|
||||
@@ -268,12 +285,9 @@ KYU_RANKS = (
|
||||
(15, 7),
|
||||
(10, 8),
|
||||
(5, 9),
|
||||
(0, 10),
|
||||
(-1, 10),
|
||||
)
|
||||
|
||||
DAN_ALLOW_DROP_DOWN = True
|
||||
MIN_HANCHANS_FOR_LADDER = 5
|
||||
|
||||
try:
|
||||
from .local_settings import * # Ignore PyLintBear (W0401, W0614)
|
||||
except ImportError:
|
||||
|
||||
File diff suppressed because one or more lines are too long
4
src/kasu/templates/django/forms/widgets/date.html
Normal file
4
src/kasu/templates/django/forms/widgets/date.html
Normal file
@@ -0,0 +1,4 @@
|
||||
{% with type="date" %}
|
||||
{% include "django/forms/widgets/html5input.html" %}
|
||||
{% endwith %}
|
||||
|
||||
4
src/kasu/templates/django/forms/widgets/datetime.html
Normal file
4
src/kasu/templates/django/forms/widgets/datetime.html
Normal file
@@ -0,0 +1,4 @@
|
||||
{% with type="datetime-local" %}
|
||||
{% include "django/forms/widgets/html5input.html" %}
|
||||
{% endwith %}
|
||||
|
||||
1
src/kasu/templates/django/forms/widgets/html5input.html
Normal file
1
src/kasu/templates/django/forms/widgets/html5input.html
Normal 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" %} />
|
||||
4
src/kasu/templates/django/forms/widgets/time.html
Normal file
4
src/kasu/templates/django/forms/widgets/time.html
Normal file
@@ -0,0 +1,4 @@
|
||||
{% with type="time" %}
|
||||
{% include "django/forms/widgets/html5input.html" %}
|
||||
{% endwith %}
|
||||
|
||||
@@ -32,7 +32,7 @@ urlpatterns = [ # Ignore PyLintBear (C0103)
|
||||
url(r'^add_page/(?P<path>[\+\.\-\d\w\/]+)/$',
|
||||
views.PageAddForm.as_view(), name='add-page'),
|
||||
url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
url(r'^admin/', admin.site.urls),
|
||||
url(r'^ckeditor/', include('ckeditor_uploader.urls')),
|
||||
url(r'^comments/', include('django_comments.urls')),
|
||||
url(r'^edit_page/(?P<path>[\+\.\-\d\w\/]+)/$',
|
||||
|
||||
147
src/kasu/xlsx.py
Normal file
147
src/kasu/xlsx.py
Normal 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)
|
||||
@@ -21,8 +21,6 @@ def recalculate(modeladmin, request, queryset): # Ignore PyLintBear (W0613)
|
||||
for ladder_ranking in queryset:
|
||||
set_dirty(user=ladder_ranking.user_id,
|
||||
season=ladder_ranking.season)
|
||||
|
||||
|
||||
recalculate.short_description = _("Recalculate")
|
||||
|
||||
|
||||
|
||||
@@ -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())))
|
||||
126
src/mahjong_ranking/management/commands/exportranking.py
Normal file
126
src/mahjong_ranking/management/commands/exportranking.py
Normal 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()
|
||||
38
src/mahjong_ranking/management/commands/resetdanranking.py
Normal file
38
src/mahjong_ranking/management/commands/resetdanranking.py
Normal 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()
|
||||
@@ -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)}
|
||||
)
|
||||
42
src/mahjong_ranking/management/commands/updateranking.py
Normal file
42
src/mahjong_ranking/management/commands/updateranking.py
Normal 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)
|
||||
@@ -1,7 +1,8 @@
|
||||
"""ObjectManagers for the Django Models used in the Mahjong-Ranking."""
|
||||
from datetime import date
|
||||
|
||||
from . import LOGGER
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class HanchanManager(models.Manager):
|
||||
@@ -12,23 +13,31 @@ class HanchanManager(models.Manager):
|
||||
"""
|
||||
use_for_related_fields = True
|
||||
|
||||
def confirmed_hanchans(self, user=None, **filter_args):
|
||||
def confirmed(self, user=None, since=None, until=None, **filter_args):
|
||||
""" Return all valid and confirmed Hanchans.
|
||||
|
||||
:param user: Only return Hanchans where this user participated.
|
||||
:param user: Only return Hanchans where this user participated.
|
||||
:param since: only return Hanchans played since the given datetime
|
||||
:param until: only return Hanchans played until the given datetime
|
||||
:param filter_args: To add specific arguments to the Django filter.
|
||||
:return: QuerySet Object
|
||||
"""
|
||||
if user:
|
||||
return self.user_hanchans(user, confirmed=True, **filter_args)
|
||||
else:
|
||||
return self.filter(confirmed=True, **filter_args)
|
||||
return self.user_hanchans(user, confirmed=True, until=until,
|
||||
**filter_args)
|
||||
hanchans = self.filter(confirmed=True, **filter_args)
|
||||
if since:
|
||||
hanchans = hanchans.filter(start__gt=since)
|
||||
if until:
|
||||
hanchans = hanchans.filter(start__lte=until)
|
||||
return hanchans
|
||||
|
||||
def dan_hanchans(self, user, **filter_args):
|
||||
def dan_hanchans(self, user, since = None, **filter_args):
|
||||
""" Return all Hanchans where a specific user has participated and had
|
||||
gain dan points and make his gamestats availabale.
|
||||
|
||||
:param user: Only return Hanchans where this user participated.
|
||||
:param since: only return Hanchans played since the given datetime
|
||||
:param filter_args: To add specific arguments to the Django filter.
|
||||
:return: QuerySet Object
|
||||
"""
|
||||
@@ -38,15 +47,18 @@ class HanchanManager(models.Manager):
|
||||
models.Q(player3=user, player3_dan_points__isnull=False) |
|
||||
models.Q(player4=user, player4_dan_points__isnull=False)
|
||||
).filter(confirmed=True, **filter_args)
|
||||
if since:
|
||||
queryset = queryset.filter(start__gt=since)
|
||||
queryset = queryset.select_related().order_by('-start')
|
||||
[ hanchan.get_playerdata(user) for hanchan in queryset ]
|
||||
[hanchan.get_playerdata(user) for hanchan in queryset]
|
||||
return queryset
|
||||
|
||||
def kyu_hanchans(self, user, **filter_args):
|
||||
def kyu_hanchans(self, user, since = None, **filter_args):
|
||||
""" Return all Hanchans where a specific user has participated and had
|
||||
gain kyū points and make his gamestats availabale.
|
||||
|
||||
:param user: Only return Hanchans where this user participated.
|
||||
:param since: only return Hanchans played since the given datetime
|
||||
:param filter_args: To add specific arguments to the Django filter.
|
||||
:return: QuerySet Object
|
||||
"""
|
||||
@@ -56,25 +68,30 @@ class HanchanManager(models.Manager):
|
||||
models.Q(player3=user, player3_kyu_points__isnull=False) |
|
||||
models.Q(player4=user, player4_kyu_points__isnull=False)
|
||||
).filter(confirmed=True, **filter_args)
|
||||
if since:
|
||||
queryset = queryset.filter(start__gt=since)
|
||||
queryset = queryset.select_related().order_by('-start')
|
||||
[ hanchan.get_playerdata(user) for hanchan in queryset ]
|
||||
[hanchan.get_playerdata(user) for hanchan in queryset]
|
||||
return queryset
|
||||
|
||||
def season_hanchans(self, user=None, season=None):
|
||||
def season_hanchans(self, user=None, season=None, until=None):
|
||||
"""Return all Hanchans that belong to a given or the current season.
|
||||
|
||||
:param user: Only return Hanchans where this user participated.
|
||||
:param season: the year of the wanted season, current year if None.
|
||||
:return: QuerySet Object
|
||||
"""
|
||||
season = season or date.today().year
|
||||
return self.confirmed_hanchans(user=user, season=season)
|
||||
try:
|
||||
season = season or until.year
|
||||
except AttributeError:
|
||||
season = date.today().year
|
||||
return self.confirmed(user=user, season=season, until=until)
|
||||
|
||||
def user_hanchans(self, user, since=None, **filter_args):
|
||||
def user_hanchans(self, user, since=None, until=None, **filter_args):
|
||||
"""Return all Hanchans where a specific user has participated.
|
||||
|
||||
:param user: Return Hanchans where this user participated.
|
||||
:param since: optional a date value since when you want to hanchans
|
||||
:param since: only return Hanchans played since the given datetime
|
||||
:param filter_args: To add specific arguments to the Django filter.
|
||||
:return: a QuerySet Object
|
||||
"""
|
||||
@@ -82,15 +99,16 @@ class HanchanManager(models.Manager):
|
||||
models.Q(player1=user) | models.Q(player2=user) |
|
||||
models.Q(player3=user) | models.Q(player4=user)
|
||||
)
|
||||
queryset = queryset.filter(**filter_args)
|
||||
if since:
|
||||
queryset = queryset.filter(start__gte=since, **filter_args)
|
||||
else:
|
||||
queryset = queryset.filter(**filter_args)
|
||||
queryset = queryset.filter(start__gte=since)
|
||||
if until:
|
||||
queryset = queryset.filter(start__lte=until)
|
||||
queryset = queryset.select_related().order_by('-start')
|
||||
[ hanchan.get_playerdata(user) for hanchan in queryset ]
|
||||
[hanchan.get_playerdata(user) for hanchan in queryset]
|
||||
return queryset
|
||||
|
||||
def unconfirmed_hanchans(self, user=None, **filter_args):
|
||||
def unconfirmed(self, user=None, **filter_args):
|
||||
""" Return all Hanchans that have been set to unconfirmed.
|
||||
|
||||
:param user: Only return Hanchans where this user participated.
|
||||
@@ -158,8 +176,27 @@ class SeasonRankingManager(models.Manager):
|
||||
})
|
||||
return json_data
|
||||
|
||||
class KyuDanRankingManager(models.Manager):
|
||||
def update(self, season=None, until=None, force_recalc=False):
|
||||
try:
|
||||
season = season or until.year
|
||||
except AttributeError:
|
||||
season = date.today().year
|
||||
if until or force_recalc:
|
||||
for ranking in self.filter(season=season):
|
||||
ranking.recalculate(until=until)
|
||||
for placement, ranking in enumerate(self.season_rankings(season), start=1):
|
||||
ranking.placement = placement
|
||||
ranking.save(force_update=True, update_fields=['placement'])
|
||||
|
||||
def season_rankings(self, season=None):
|
||||
season = season or date.today().year
|
||||
rankings = self.filter(
|
||||
season=season,
|
||||
hanchan_count__gt=settings.MIN_HANCHANS_FOR_LADDER)
|
||||
return rankings.order_by('avg_placement', '-avg_score')
|
||||
|
||||
|
||||
class KyuDanRankingManager(models.Manager):
|
||||
def json_data(self):
|
||||
""" Get all Rankings for a given Season and return them as a list of
|
||||
dict objects, suitable for JSON exports and other processings.
|
||||
@@ -172,9 +209,12 @@ class KyuDanRankingManager(models.Manager):
|
||||
values = values.values('user_id', 'user__username',
|
||||
'user__first_name', 'user__last_name',
|
||||
'dan', 'dan_points', 'kyu', 'kyu_points',
|
||||
'hanchan_count', 'won_hanchans', 'good_hanchans')
|
||||
'hanchan_count', 'won_hanchans', 'good_hanchans',
|
||||
'last_hanchan_date')
|
||||
for user in values:
|
||||
if user['dan']:
|
||||
if user['hanchan_count'] == 0:
|
||||
continue
|
||||
elif user['dan']:
|
||||
rank = '{}. Dan'.format(user['dan'])
|
||||
points = user['dan_points']
|
||||
else:
|
||||
@@ -183,11 +223,31 @@ class KyuDanRankingManager(models.Manager):
|
||||
json_data.append({
|
||||
'user_id': user['user_id'],
|
||||
'username': user['user__username'],
|
||||
'full_name': " ".join([user['user__last_name'], user['user__first_name']]),
|
||||
'full_name': " ".join(
|
||||
[user['user__last_name'], user['user__first_name']]),
|
||||
'rank': rank,
|
||||
'points': points,
|
||||
'hanchan_count': user['hanchan_count'],
|
||||
'good_hanchans': user['good_hanchans'],
|
||||
'won_hanchans': user['won_hanchans']
|
||||
'won_hanchans': user['won_hanchans'],
|
||||
'last_hanchan_date': user['last_hanchan_date']
|
||||
|
||||
})
|
||||
return json_data
|
||||
|
||||
def update(self, since=None, until=None, force_recalc=False):
|
||||
old_attr = {'dan': None, 'dan_points': None,
|
||||
'kyu': None, 'kyu_points': None, 'won_hanchans': None,
|
||||
'good_hanchans': None, 'hanchan_count': None}
|
||||
for ranking in self.all():
|
||||
old_attr = {attr: getattr(ranking, attr) for attr in
|
||||
old_attr.keys()}
|
||||
ranking.calculate(since=since, until=until,
|
||||
force_recalc=force_recalc)
|
||||
for attr, old_value in old_attr.items():
|
||||
if getattr(ranking, attr) != old_value:
|
||||
LOGGER.warning(
|
||||
"%(user)s recalc shows differences in %(attr)s! old: %(old)d, new: %(new)d",
|
||||
{'user': ranking.user, 'attr': attr,
|
||||
'old': old_value or 0, 'new': getattr(ranking, attr) or 0}
|
||||
)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"""Middleware to defer slow denormalization at the end of a request."""
|
||||
from django.core.cache import cache
|
||||
|
||||
from mahjong_ranking import models
|
||||
from . import LOGGER, MIN_HANCHANS_FOR_LADDER
|
||||
from . import LOGGER
|
||||
|
||||
|
||||
class DenormalizationUpdateMiddleware(object): # Ignore PyLintBear (R0903)
|
||||
@@ -35,7 +34,7 @@ class DenormalizationUpdateMiddleware(object): # Ignore PyLintBear (R0903)
|
||||
user_id, hanchan_start = kyu_dan_ranking_queue.pop()
|
||||
kyu_dan_ranking = models.KyuDanRanking.objects.get_or_create(
|
||||
user_id=user_id)[0]
|
||||
kyu_dan_ranking.recalculate(hanchan_start)
|
||||
kyu_dan_ranking.calculate(since=hanchan_start)
|
||||
cache.set('kyu_dan_ranking_queue', set(), 360)
|
||||
|
||||
ladder_ranking_queue = cache.get('ladder_ranking_queue', set())
|
||||
@@ -58,12 +57,5 @@ class DenormalizationUpdateMiddleware(object): # Ignore PyLintBear (R0903)
|
||||
|
||||
for season in season_queue:
|
||||
LOGGER.info(u'Recalculate placements for Season %d', season)
|
||||
season_rankings = models.SeasonRanking.objects.filter(
|
||||
season=season, hanchan_count__gt=MIN_HANCHANS_FOR_LADDER
|
||||
).order_by('avg_placement', '-avg_score')
|
||||
placement = 1
|
||||
for ranking in season_rankings:
|
||||
ranking.placement = placement
|
||||
ranking.save(force_update=True)
|
||||
placement += 1
|
||||
models.SeasonRanking.objects.update(season=season)
|
||||
return response
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('events', '0005_auto_20150907_2021'),
|
||||
@@ -17,15 +16,19 @@ class Migration(migrations.Migration):
|
||||
name='EventRanking',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID',
|
||||
serialize=False, auto_created=True, primary_key=True)),
|
||||
('placement', models.PositiveIntegerField(null=True, blank=True)),
|
||||
serialize=False, auto_created=True,
|
||||
primary_key=True)),
|
||||
('placement',
|
||||
models.PositiveIntegerField(null=True, blank=True)),
|
||||
('avg_placement', models.FloatField(default=4)),
|
||||
('avg_score', models.FloatField(default=0)),
|
||||
('hanchan_count', models.PositiveIntegerField(default=0)),
|
||||
('good_hanchans', models.PositiveIntegerField(default=0)),
|
||||
('won_hanchans', models.PositiveIntegerField(default=0)),
|
||||
('event', models.ForeignKey(to='events.Event')),
|
||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
|
||||
('event', models.ForeignKey(to='events.Event',
|
||||
on_delete=models.CASCADE)),
|
||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'ordering': ('placement', 'avg_placement', '-avg_score'),
|
||||
@@ -35,10 +38,13 @@ class Migration(migrations.Migration):
|
||||
name='Hanchan',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID',
|
||||
serialize=False, auto_created=True, primary_key=True)),
|
||||
serialize=False, auto_created=True,
|
||||
primary_key=True)),
|
||||
('start', models.DateTimeField(
|
||||
help_text='Wichtig damit die richtigen Hanchans in die Wertung kommen.', verbose_name='Beginn')),
|
||||
('player1_input_score', models.IntegerField(verbose_name='Punkte')),
|
||||
help_text='Wichtig damit die richtigen Hanchans in die Wertung kommen.',
|
||||
verbose_name='Beginn')),
|
||||
('player1_input_score',
|
||||
models.IntegerField(verbose_name='Punkte')),
|
||||
('player1_game_score', models.PositiveIntegerField(
|
||||
default=0, verbose_name='Punkte', editable=False)),
|
||||
('player1_placement', models.PositiveSmallIntegerField(
|
||||
@@ -50,8 +56,11 @@ class Migration(migrations.Migration):
|
||||
('player1_bonus_points', models.SmallIntegerField(
|
||||
null=True, editable=False, blank=True)),
|
||||
('player1_comment', models.CharField(verbose_name='Anmerkung',
|
||||
max_length=255, editable=False, blank=True)),
|
||||
('player2_input_score', models.IntegerField(verbose_name='Punkte')),
|
||||
max_length=255,
|
||||
editable=False,
|
||||
blank=True)),
|
||||
('player2_input_score',
|
||||
models.IntegerField(verbose_name='Punkte')),
|
||||
('player2_game_score', models.PositiveIntegerField(
|
||||
default=0, verbose_name='Punkte', editable=False)),
|
||||
('player2_placement', models.PositiveSmallIntegerField(
|
||||
@@ -63,8 +72,11 @@ class Migration(migrations.Migration):
|
||||
('player2_bonus_points', models.SmallIntegerField(
|
||||
null=True, editable=False, blank=True)),
|
||||
('player2_comment', models.CharField(verbose_name='Anmerkung',
|
||||
max_length=255, editable=False, blank=True)),
|
||||
('player3_input_score', models.IntegerField(verbose_name='Punkte')),
|
||||
max_length=255,
|
||||
editable=False,
|
||||
blank=True)),
|
||||
('player3_input_score',
|
||||
models.IntegerField(verbose_name='Punkte')),
|
||||
('player3_game_score', models.PositiveIntegerField(
|
||||
default=0, verbose_name='Punkte', editable=False)),
|
||||
('player3_placement', models.PositiveSmallIntegerField(
|
||||
@@ -76,8 +88,11 @@ class Migration(migrations.Migration):
|
||||
('player3_bonus_points', models.SmallIntegerField(
|
||||
null=True, editable=False, blank=True)),
|
||||
('player3_comment', models.CharField(verbose_name='Anmerkung',
|
||||
max_length=255, editable=False, blank=True)),
|
||||
('player4_input_score', models.IntegerField(verbose_name='Punkte')),
|
||||
max_length=255,
|
||||
editable=False,
|
||||
blank=True)),
|
||||
('player4_input_score',
|
||||
models.IntegerField(verbose_name='Punkte')),
|
||||
('player4_game_score', models.PositiveIntegerField(
|
||||
default=0, verbose_name='Punkte', editable=False)),
|
||||
('player4_placement', models.PositiveSmallIntegerField(
|
||||
@@ -89,22 +104,37 @@ class Migration(migrations.Migration):
|
||||
('player4_bonus_points', models.SmallIntegerField(
|
||||
null=True, editable=False, blank=True)),
|
||||
('player4_comment', models.CharField(verbose_name='Anmerkung',
|
||||
max_length=255, editable=False, blank=True)),
|
||||
('comment', models.TextField(verbose_name='Anmerkung', blank=True)),
|
||||
max_length=255,
|
||||
editable=False,
|
||||
blank=True)),
|
||||
('comment',
|
||||
models.TextField(verbose_name='Anmerkung', blank=True)),
|
||||
('confirmed', models.BooleanField(
|
||||
default=True, help_text='Nur g\xfcltige und best\xe4tigte Hanchans kommen in die Wertung.', verbose_name='Wurde best\xe4tigt')),
|
||||
('player_names', models.CharField(max_length=255, editable=False)),
|
||||
default=True,
|
||||
help_text='Nur g\xfcltige und best\xe4tigte Hanchans kommen in die Wertung.',
|
||||
verbose_name='Wurde best\xe4tigt')),
|
||||
('player_names',
|
||||
models.CharField(max_length=255, editable=False)),
|
||||
('season', models.PositiveSmallIntegerField(
|
||||
verbose_name='Saison', editable=False, db_index=True)),
|
||||
('event', models.ForeignKey(to='events.Event')),
|
||||
('event', models.ForeignKey(to='events.Event',
|
||||
on_delete=models.CASCADE)),
|
||||
('player1', models.ForeignKey(related_name='user_hanchan+',
|
||||
verbose_name='Spieler 1', to=settings.AUTH_USER_MODEL)),
|
||||
verbose_name='Spieler 1',
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE)),
|
||||
('player2', models.ForeignKey(related_name='user_hanchan+',
|
||||
verbose_name='Spieler 2', to=settings.AUTH_USER_MODEL)),
|
||||
verbose_name='Spieler 2',
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE)),
|
||||
('player3', models.ForeignKey(related_name='user_hanchan+',
|
||||
verbose_name='Spieler 3', to=settings.AUTH_USER_MODEL)),
|
||||
verbose_name='Spieler 3',
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE)),
|
||||
('player4', models.ForeignKey(related_name='user_hanchan+',
|
||||
verbose_name='Spieler 4', to=settings.AUTH_USER_MODEL)),
|
||||
verbose_name='Spieler 4',
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'ordering': ('-start',),
|
||||
@@ -116,8 +146,11 @@ class Migration(migrations.Migration):
|
||||
name='KyuDanRanking',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID',
|
||||
serialize=False, auto_created=True, primary_key=True)),
|
||||
('dan', models.PositiveSmallIntegerField(null=True, blank=True)),
|
||||
serialize=False, auto_created=True,
|
||||
primary_key=True)),
|
||||
(
|
||||
'dan',
|
||||
models.PositiveSmallIntegerField(null=True, blank=True)),
|
||||
('dan_points', models.PositiveIntegerField(default=0)),
|
||||
('kyu', models.PositiveSmallIntegerField(
|
||||
default=10, null=True, blank=True)),
|
||||
@@ -128,7 +161,8 @@ class Migration(migrations.Migration):
|
||||
('legacy_date', models.DateField(null=True, blank=True)),
|
||||
('legacy_dan_points', models.PositiveIntegerField(default=0)),
|
||||
('legacy_kyu_points', models.PositiveIntegerField(default=0)),
|
||||
('user', models.OneToOneField(to=settings.AUTH_USER_MODEL)),
|
||||
('user', models.OneToOneField(to=settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'ordering': ('-dan', '-dan_points', '-kyu_points'),
|
||||
@@ -140,15 +174,19 @@ class Migration(migrations.Migration):
|
||||
name='SeasonRanking',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID',
|
||||
serialize=False, auto_created=True, primary_key=True)),
|
||||
('season', models.PositiveSmallIntegerField(verbose_name='Saison')),
|
||||
('placement', models.PositiveIntegerField(null=True, blank=True)),
|
||||
serialize=False, auto_created=True,
|
||||
primary_key=True)),
|
||||
('season',
|
||||
models.PositiveSmallIntegerField(verbose_name='Saison')),
|
||||
('placement',
|
||||
models.PositiveIntegerField(null=True, blank=True)),
|
||||
('avg_placement', models.FloatField(null=True, blank=True)),
|
||||
('avg_score', models.FloatField(null=True, blank=True)),
|
||||
('hanchan_count', models.PositiveIntegerField(default=0)),
|
||||
('good_hanchans', models.PositiveIntegerField(default=0)),
|
||||
('won_hanchans', models.PositiveIntegerField(default=0)),
|
||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
|
||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'ordering': ('placement', 'avg_placement', '-avg_score'),
|
||||
|
||||
29
src/mahjong_ranking/migrations/0005_auto_20171115_0653.py
Normal file
29
src/mahjong_ranking/migrations/0005_auto_20171115_0653.py
Normal 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),
|
||||
),
|
||||
]
|
||||
87
src/mahjong_ranking/migrations/0006_auto_20171214_1318.py
Normal file
87
src/mahjong_ranking/migrations/0006_auto_20171214_1318.py
Normal 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),
|
||||
),
|
||||
]
|
||||
18
src/mahjong_ranking/mixins.py
Normal file
18
src/mahjong_ranking/mixins.py
Normal 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
|
||||
@@ -5,10 +5,12 @@
|
||||
|
||||
from __future__ import division
|
||||
|
||||
from datetime import datetime, time
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
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,
|
||||
wenn der Event als Turnier markiert wurde.
|
||||
"""
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL)
|
||||
event = models.ForeignKey(Event)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
|
||||
event = models.ForeignKey(Event, on_delete=models.CASCADE)
|
||||
placement = models.PositiveIntegerField(blank=True, null=True)
|
||||
avg_placement = models.FloatField(default=4)
|
||||
avg_score = models.FloatField(default=0)
|
||||
@@ -56,7 +58,7 @@ class EventRanking(models.Model):
|
||||
)
|
||||
sum_placement = 0.0
|
||||
sum_score = 0.0
|
||||
event_hanchans = Hanchan.objects.confirmed_hanchans(
|
||||
event_hanchans = Hanchan.objects.confirmed(
|
||||
user=self.user_id,
|
||||
event=self.event_id
|
||||
)
|
||||
@@ -84,7 +86,7 @@ class Hanchan(models.Model):
|
||||
Es werden aber noch andere Tests durchgeführt, ob sie gültig ist.
|
||||
Außerdem gehört jede Hanchan zu einer Veranstaltung.
|
||||
"""
|
||||
event = models.ForeignKey(Event)
|
||||
event = models.ForeignKey(Event, on_delete=models.CASCADE)
|
||||
start = models.DateTimeField(
|
||||
_('Start'),
|
||||
help_text=_('This is crucial to get the right Hanchans that scores')
|
||||
@@ -92,7 +94,7 @@ class Hanchan(models.Model):
|
||||
|
||||
player1 = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='user_hanchan+',
|
||||
verbose_name=_('Player 1'))
|
||||
player1_input_score = models.IntegerField(_('Score'))
|
||||
@@ -111,7 +113,7 @@ class Hanchan(models.Model):
|
||||
|
||||
player2 = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='user_hanchan+',
|
||||
verbose_name=_('Player 2'))
|
||||
player2_input_score = models.IntegerField(_('Score'))
|
||||
@@ -130,7 +132,7 @@ class Hanchan(models.Model):
|
||||
|
||||
player3 = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='user_hanchan+',
|
||||
verbose_name=_('Player 3'))
|
||||
player3_input_score = models.IntegerField(_('Score'))
|
||||
@@ -149,7 +151,7 @@ class Hanchan(models.Model):
|
||||
|
||||
player4 = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='user_hanchan+',
|
||||
verbose_name=_('Player 4'))
|
||||
player4_input_score = models.IntegerField(_('Score'))
|
||||
@@ -333,32 +335,49 @@ class KyuDanRanking(models.Model):
|
||||
Im Gegensatz zum Ladder Ranking ist das nicht Saison gebunden.
|
||||
Deswegen läuft es getrennt.
|
||||
"""
|
||||
user = models.OneToOneField(settings.AUTH_USER_MODEL)
|
||||
user = models.OneToOneField(settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE)
|
||||
dan = models.PositiveSmallIntegerField(blank=True, null=True)
|
||||
dan_points = models.PositiveIntegerField(default=0)
|
||||
max_dan_points = models.PositiveIntegerField(default=0)
|
||||
kyu = models.PositiveSmallIntegerField(default=10, blank=True, null=True)
|
||||
kyu_points = models.PositiveIntegerField(default=0)
|
||||
won_hanchans = models.PositiveIntegerField(default=0)
|
||||
good_hanchans = models.PositiveIntegerField(default=0)
|
||||
hanchan_count = models.PositiveIntegerField(default=0)
|
||||
legacy_date = models.DateField(blank=True, null=True)
|
||||
legacy_hanchan_count = models.PositiveIntegerField(default=0)
|
||||
legacy_dan_points = models.PositiveIntegerField(default=0)
|
||||
legacy_kyu_points = models.PositiveIntegerField(default=0)
|
||||
wins_in_a_row = 0
|
||||
legacy_dan = models.PositiveSmallIntegerField(blank=True, null=True)
|
||||
legacy_dan_points = models.PositiveIntegerField(blank=True, null=True)
|
||||
legacy_kyu = models.PositiveSmallIntegerField(blank=True, null=True)
|
||||
legacy_kyu_points = models.PositiveIntegerField(blank=True, null=True)
|
||||
legacy_hanchan_count = models.PositiveIntegerField(blank=True, null=True)
|
||||
legacy_good_hanchans = models.PositiveIntegerField(blank=True, null=True)
|
||||
legacy_won_hanchans = models.PositiveIntegerField(blank=True, null=True)
|
||||
wins_in_a_row = models.PositiveIntegerField(default=0)
|
||||
last_hanchan_date = models.DateTimeField(blank=True, null=True)
|
||||
objects = managers.KyuDanRankingManager()
|
||||
|
||||
|
||||
class Meta(object):
|
||||
ordering = ('-dan_points', 'dan', '-kyu_points')
|
||||
verbose_name = _(u'Kyū/Dan Ranking')
|
||||
verbose_name_plural = _(u'Kyū/Dan Rankings')
|
||||
|
||||
def __unicode__(self):
|
||||
if self.dan_points is not None:
|
||||
@property
|
||||
def rank(self):
|
||||
if self.dan is not None:
|
||||
return "{0:d}. Dan".format(self.dan)
|
||||
else:
|
||||
return "{0:d}. Kyū".format(self.kyu or 10)
|
||||
|
||||
@property
|
||||
def points(self):
|
||||
return self.dan_points if self.dan is not None else self.kyu_points
|
||||
|
||||
def __str__(self):
|
||||
if self.dan is not None:
|
||||
return u"%s - %d. Dan" % (self.user.username, self.dan or 1)
|
||||
else:
|
||||
return u"%s - %d. Kyu" % (self.user.username, self.kyu or 10)
|
||||
return u"%s - %d. Kyū" % (self.user.username, self.kyu or 10)
|
||||
|
||||
def append_3_in_a_row_bonuspoints(self, hanchan):
|
||||
u"""
|
||||
@@ -366,12 +385,14 @@ class KyuDanRanking(models.Model):
|
||||
das er einen Dan Rang aufsteigt. Dies wird als Kommentar abgespeichert,
|
||||
um es besser nachvollziehen zu können.
|
||||
"""
|
||||
if self.dan and hanchan.placement == 1:
|
||||
if not self.dan or not settings.DAN_3_WINS_IN_A_ROW:
|
||||
return
|
||||
if hanchan.placement == 1:
|
||||
self.wins_in_a_row += 1
|
||||
else:
|
||||
self.wins_in_a_row = 0
|
||||
|
||||
if self.dan and self.wins_in_a_row >= 3 and self.dan < 9:
|
||||
return
|
||||
if self.wins_in_a_row >= 3 and self.dan < 9:
|
||||
LOGGER.info(
|
||||
'adding bonuspoints for 3 wins in a row for %s', self.user)
|
||||
new_dan_rank = self.dan + 1
|
||||
@@ -392,8 +413,8 @@ class KyuDanRanking(models.Model):
|
||||
bonus_points, new_dan_rank)
|
||||
self.dan_points += bonus_points
|
||||
self.wins_in_a_row = 0
|
||||
self.update_rank()
|
||||
|
||||
# TODO: Komplett Überabreiten!
|
||||
def append_tournament_bonuspoints(self, hanchan):
|
||||
"""
|
||||
Prüft ob es die letzte Hanchan in einem Turnier war. Wenn ja werden
|
||||
@@ -406,29 +427,29 @@ class KyuDanRanking(models.Model):
|
||||
user=self.user, event=hanchan.event
|
||||
).order_by('-start')
|
||||
last_hanchan_this_event = hanchans_this_event[0]
|
||||
if hanchan != last_hanchan_this_event:
|
||||
# Das braucht nur am Ende eines Turnieres gemacht werden.
|
||||
return False
|
||||
else:
|
||||
event_ranking = EventRanking.objects.get(
|
||||
user=self.user,
|
||||
event=hanchan.event
|
||||
)
|
||||
if event_ranking.placement == 1:
|
||||
bonus_points += 4
|
||||
hanchan.player_comment += u'+4 Punkte Turnier gewonnen. '
|
||||
if event_ranking.avg_placement == 1:
|
||||
bonus_points += 8
|
||||
hanchan.player_comment += u'+8 Pkt: alle Spiele des Turnieres gewonnen. '
|
||||
# Das braucht nur am Ende eines Turnieres gemacht werden.
|
||||
if hanchan != last_hanchan_this_event: return False
|
||||
event_ranking = EventRanking.objects.get(
|
||||
user=self.user,
|
||||
event=hanchan.event
|
||||
)
|
||||
if event_ranking.placement == 1:
|
||||
bonus_points += settings.TOURNAMENT_WIN_BONUSPOINTS
|
||||
hanchan.player_comment += u'+{0:d} Punkte Turnier gewonnen. '.format(
|
||||
settings.TOURNAMENT_WIN_BONUSPOINTS)
|
||||
if event_ranking.avg_placement == 1:
|
||||
bonus_points += settings.TOURNAMENT_FLAWLESS_VICTORY_BONUSPOINTS
|
||||
hanchan.player_comment += u'+{0:d} Pkt: alle Spiele des Turnieres gewonnen. '.format(
|
||||
settings.TOURNAMENT_FLAWLESS_VICTORY_BONUSPOINTS)
|
||||
|
||||
if bonus_points and self.dan:
|
||||
hanchan.dan_points += bonus_points
|
||||
self.dan_points += bonus_points
|
||||
elif bonus_points:
|
||||
hanchan.kyu_points += bonus_points
|
||||
self.kyu_points += bonus_points
|
||||
hanchan.bonus_points += bonus_points
|
||||
return True
|
||||
if bonus_points and self.dan:
|
||||
hanchan.dan_points += bonus_points
|
||||
self.dan_points += bonus_points
|
||||
elif bonus_points:
|
||||
hanchan.kyu_points += bonus_points
|
||||
self.kyu_points += bonus_points
|
||||
hanchan.bonus_points += bonus_points
|
||||
return True
|
||||
|
||||
def get_absolute_url(self):
|
||||
if self.dan or self.dan_points > 0:
|
||||
@@ -436,62 +457,67 @@ class KyuDanRanking(models.Model):
|
||||
else:
|
||||
return reverse('player-kyu-score', args=[self.user.username])
|
||||
|
||||
def recalculate(self, hanchan_start=None):
|
||||
def calculate(self, since=None, until=None, force_recalc=False):
|
||||
"""
|
||||
Fetches all valid Hanchans from this Player and recalculates his
|
||||
Kyu/Dan Ranking.
|
||||
"""
|
||||
self.dan = None
|
||||
self.dan_points = self.legacy_dan_points or 0
|
||||
self.kyu = None
|
||||
self.kyu_points = self.legacy_kyu_points or 0
|
||||
self.hanchan_count = self.legacy_hanchan_count or 0
|
||||
self.good_hanchans = 0
|
||||
self.won_hanchans = 0
|
||||
self.update_rank()
|
||||
|
||||
valid_hanchans = Hanchan.objects.confirmed(user=self.user)
|
||||
valid_hanchans = valid_hanchans.order_by('start')
|
||||
if since and self.last_hanchan_date and since < self.last_hanchan_date:
|
||||
force_recalc = True
|
||||
if until and self.last_hanchan_date and until < self.last_hanchan_date:
|
||||
force_recalc = True
|
||||
if force_recalc:
|
||||
# Setze alles auf die legacy Werte und berechne alles von neuem.
|
||||
self.dan = 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(
|
||||
"recalculating Kyu/Dan points for %s since %s...",
|
||||
self.user, str(hanchan_start)
|
||||
"recalculating Kyu/Dan points for %(user)s since %(since)s...",
|
||||
{'user': self.user, 'since': str(since)}
|
||||
)
|
||||
valid_hanchans = Hanchan.objects.confirmed_hanchans(
|
||||
user=self.user).order_by('start')
|
||||
if self.legacy_date:
|
||||
valid_hanchans = valid_hanchans.filter(start__gt=self.legacy_date)
|
||||
|
||||
""" TODO: Hanchan Punkte nur neu berechnen wenn sie nach hachan_start
|
||||
lagen. Es müssen aber alle durch die Schleife rennen, damit die Punkte
|
||||
richtig gezählt werden."""
|
||||
self.hanchan_count += valid_hanchans.count()
|
||||
if since:
|
||||
valid_hanchans = valid_hanchans.filter(start__gt=since)
|
||||
if until:
|
||||
valid_hanchans = valid_hanchans.filter(start__lte=until)
|
||||
for hanchan in valid_hanchans:
|
||||
self.hanchan_count += 1
|
||||
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.kyu_points += hanchan.kyu_points or 0
|
||||
self.update_rank()
|
||||
else:
|
||||
hanchan.bonus_points = 0
|
||||
hanchan.player_comment = u""
|
||||
hanchan.player_comment = ""
|
||||
self.update_hanchan_points(hanchan)
|
||||
if hanchan.event.mahjong_tournament:
|
||||
self.append_tournament_bonuspoints(hanchan)
|
||||
self.update_rank()
|
||||
self.append_3_in_a_row_bonuspoints(hanchan)
|
||||
self.update_rank()
|
||||
hanchan.update_playerdata(self.user)
|
||||
hanchan.save(recalculate=False)
|
||||
self.won_hanchans += 1 if hanchan.placement == 1 else 0
|
||||
self.good_hanchans += 1 if hanchan.placement == 2 else 0
|
||||
LOGGER.debug(
|
||||
'id: %(id)d, start: %(start)s, placement: %(placement)d, '
|
||||
'score: %(score)d, kyu points: %(kyu_points)d, dan points: '
|
||||
'%(dan_points)d, bonus points: %(bonus_points)d',
|
||||
{'id': hanchan.pk, 'start': hanchan.start,
|
||||
'placement': hanchan.placement, 'score': hanchan.game_score,
|
||||
'kyu_points': hanchan.kyu_points or 0,
|
||||
'dan_points': hanchan.dan_points or 0,
|
||||
'bonus_points': hanchan.bonus_points or 0}
|
||||
)
|
||||
self.last_hanchan_date = hanchan.start
|
||||
self.save(force_update=True)
|
||||
|
||||
def update_hanchan_points(self, hanchan):
|
||||
@@ -502,7 +528,7 @@ class KyuDanRanking(models.Model):
|
||||
"""
|
||||
hanchan.kyu_points = None
|
||||
hanchan.dan_points = None
|
||||
if hanchan.event.mahjong_tournament:
|
||||
if hanchan.event.mahjong_tournament and settings.TOURNAMENT_POINT_SYSTEM:
|
||||
"""Für Turniere gelten andere Regeln zur Punktevergabe:
|
||||
1. Platz 4 Punkte
|
||||
2. Platz 3 Punkte
|
||||
@@ -526,6 +552,7 @@ class KyuDanRanking(models.Model):
|
||||
hanchan.dan_points = -1
|
||||
elif hanchan.placement == 4:
|
||||
hanchan.dan_points = -2
|
||||
# otherwise player must be in the kyu ranking
|
||||
elif hanchan.game_score >= 60000:
|
||||
hanchan.kyu_points = 3
|
||||
elif hanchan.game_score >= 30000:
|
||||
@@ -539,45 +566,44 @@ class KyuDanRanking(models.Model):
|
||||
if self.dan:
|
||||
# Only substract so much points that player has 0 Points:
|
||||
if self.dan_points + hanchan.dan_points < 0:
|
||||
hanchan.player_comment = 'Spieler unterschreitet 0 Punkte.' \
|
||||
'(Original {} Punkte)'.format(hanchan.dan_points)
|
||||
hanchan.dan_points -= (self.dan_points + hanchan.dan_points)
|
||||
self.dan_points += hanchan.dan_points
|
||||
else:
|
||||
# Only substract so much points that player has 0 Points:
|
||||
if self.kyu_points + hanchan.kyu_points < 0:
|
||||
hanchan.player_comment = 'Spieler unterschreitet 0 Punkte.' \
|
||||
'(Original {} Punkte)'.format(hanchan.kyu_points)
|
||||
hanchan.kyu_points -= (self.kyu_points + hanchan.kyu_points)
|
||||
self.kyu_points += hanchan.kyu_points
|
||||
|
||||
# TODO: Merkwürdige Methode die zwar funktioniert aber nicht sehr
|
||||
# aussagekräfig ist. Überarbeiten?
|
||||
def update_rank(self):
|
||||
if self.dan and self.dan_points < 0:
|
||||
self.dan_points = 0
|
||||
self.dan = 1
|
||||
elif self.dan or self.dan_points > 0:
|
||||
old_dan = self.dan
|
||||
for min_points, dan_rank in settings.DAN_RANKS:
|
||||
if self.dan_points > min_points:
|
||||
self.dan = dan_rank
|
||||
break
|
||||
if old_dan is None or self.dan > old_dan:
|
||||
self.wins_in_a_row = 0
|
||||
elif self.kyu_points < 1:
|
||||
self.kyu_points = 0
|
||||
self.kyu = 10
|
||||
# Update Dan ranking:
|
||||
if self.dan or self.dan_points > 0:
|
||||
if settings.DAN_ALLOW_DROP_DOWN:
|
||||
self.dan = max((dan for min_points, dan in settings.DAN_RANKS
|
||||
if self.dan_points > min_points))
|
||||
else:
|
||||
self.max_dan_points = max(self.max_dan_points, self.dan_points)
|
||||
self.dan = max((dan for min_points, dan in settings.DAN_RANKS
|
||||
if self.max_dan_points > min_points))
|
||||
|
||||
# jump from Kyu to Dan
|
||||
elif self.kyu_points > 50:
|
||||
self.dan = 1
|
||||
self.kyu = None
|
||||
self.dan_points = 0
|
||||
self.kyu = None
|
||||
self.kyu_points = 0
|
||||
self.wins_in_a_row = 0
|
||||
# update Kyu ranking_
|
||||
else:
|
||||
for min_points, kyu_rank in settings.KYU_RANKS:
|
||||
if self.kyu_points > min_points:
|
||||
self.kyu = kyu_rank
|
||||
break
|
||||
self.kyu = min((kyu for min_points, kyu in settings.KYU_RANKS
|
||||
if self.kyu_points > min_points))
|
||||
|
||||
|
||||
class SeasonRanking(models.Model):
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
|
||||
season = models.PositiveSmallIntegerField(_('Season'))
|
||||
placement = models.PositiveIntegerField(blank=True, null=True)
|
||||
avg_placement = models.FloatField(blank=True, null=True)
|
||||
@@ -593,9 +619,9 @@ class SeasonRanking(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return reverse('player-ladder-score', args=[self.user.username])
|
||||
|
||||
def recalculate(self):
|
||||
def recalculate(self, until=None):
|
||||
season_hanchans = Hanchan.objects.season_hanchans(
|
||||
user=self.user, season=self.season)
|
||||
user=self.user, season=self.season, until=until)
|
||||
sum_placement = 0
|
||||
sum_score = 0
|
||||
self.placement = None
|
||||
|
||||
@@ -50,4 +50,12 @@
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endblock %}
|
||||
|
||||
{% if kyu_dan_ranking.legacy_date %}
|
||||
<p><strong>Frühere Dan Punkte vom {{ kyu_dan_ranking.legacy_date|date }}:</strong> {{kyu_dan_ranking.legacy_dan_points }}</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block buttonbar %}
|
||||
<a href="?download=xlsx" class="button"><span class="fa fa-table"></span> Download</a>
|
||||
{% endblock %}
|
||||
|
||||
@@ -35,8 +35,12 @@
|
||||
{% if perms.mahjong_ranking.change_hanchan %}
|
||||
<a href="{% url 'edit-hanchan' hanchan.pk %}"><span class="fa fa-pencil" title="{% trans 'Edit Hanchan' %}"></span></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
{% block buttonbar %}
|
||||
<a href="?download=xlsx" class="button"><span class="fa fa-table"></span> Download</a>
|
||||
{% endblock %}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<th rowspan="2">{% trans 'Placement' %}</th>
|
||||
<th colspan="4">{% trans 'Players' %}</th>
|
||||
<th rowspan="2">{% trans 'Kyu Points' %}</th>
|
||||
<th rowspan="2"></th>
|
||||
<th rowspan="2"></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>1.</th>
|
||||
@@ -45,6 +45,10 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
{% block buttonbar %}
|
||||
<a href="?download=xlsx" class="button"><span class="fa fa-table"></span> Download</a>
|
||||
{% endblock %}
|
||||
|
||||
@@ -72,5 +72,8 @@
|
||||
</form>
|
||||
</td></tr></tfoot>
|
||||
</table>
|
||||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
||||
{% block buttonbar %}
|
||||
<a href="?download=xlsx" class="button"><span class="fa fa-table"></span> Download</a>
|
||||
{% endblock %}
|
||||
|
||||
@@ -34,7 +34,7 @@ class KyuDanTest(TestCase):
|
||||
|
||||
for ranking in KyuDanRanking.objects.all():
|
||||
original = {a: getattr(ranking, a) for a in self.equal_attrs}
|
||||
ranking.recalculate()
|
||||
ranking.calculate()
|
||||
for attr in self.equal_attrs:
|
||||
self.assertEqual(
|
||||
original[attr],
|
||||
@@ -54,7 +54,7 @@ class KyuDanTest(TestCase):
|
||||
|
||||
for ranking in KyuDanRanking.objects.all():
|
||||
original = {a: getattr(ranking, a) for a in self.equal_attrs}
|
||||
confirmed_hanchans = Hanchan.objects.confirmed_hanchans(
|
||||
confirmed_hanchans = Hanchan.objects.confirmed(
|
||||
user=ranking.user,
|
||||
since=ranking.legacy_date
|
||||
)
|
||||
@@ -62,7 +62,7 @@ class KyuDanTest(TestCase):
|
||||
continue
|
||||
rnd = random.randrange(confirmed_hanchans.count())
|
||||
since = confirmed_hanchans[rnd].start
|
||||
ranking.recalculate(hanchan_start=since)
|
||||
ranking.calculate(since=since)
|
||||
for attr in self.equal_attrs:
|
||||
self.assertEqual(
|
||||
original[attr],
|
||||
@@ -86,7 +86,7 @@ class KyuDanTest(TestCase):
|
||||
'dan_points': ranking.legacy_dan_points or 0,
|
||||
'kyu_points': ranking.legacy_kyu_points or 0
|
||||
}
|
||||
confirmed_hanchans = Hanchan.objects.confirmed_hanchans(
|
||||
confirmed_hanchans = Hanchan.objects.confirmed(
|
||||
user=ranking.user,
|
||||
since=ranking.legacy_date
|
||||
)
|
||||
|
||||
@@ -7,15 +7,16 @@ from django.contrib import auth
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, \
|
||||
PermissionRequiredMixin
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views import generic
|
||||
|
||||
from events.models import Event
|
||||
from events.mixins import EventDetailMixin
|
||||
from kasu import xlsx
|
||||
from . import forms, models
|
||||
from .mixins import MahjongMixin
|
||||
|
||||
kyu_dan_order = {
|
||||
KYU_DAN_ORDER = { # map sort URL args to Django ORM order_by args
|
||||
'+full_name': ('user__last_name', 'user__first_name'),
|
||||
'-full_name': ('-user__last_name', '-user__first_name'),
|
||||
'+hanchan_count': ('hanchan_count',),
|
||||
@@ -31,16 +32,17 @@ kyu_dan_order = {
|
||||
|
||||
class DeleteHanchan(EventDetailMixin, PermissionRequiredMixin,
|
||||
generic.DeleteView):
|
||||
"""
|
||||
Fragt zuerst nach, ob die Hanchan wirklich gelöscht werden soll.
|
||||
Wir die Frage mit "Ja" beantwortet, wird die die Hanchan gelöscht.
|
||||
"""
|
||||
"""Deletes a Hanchan if confimration has been answerd with 'yes'."""
|
||||
form_class = forms.HanchanForm
|
||||
model = models.Hanchan
|
||||
permission_required = 'mahjong_ranking.delete_hanchan'
|
||||
pk_url_kwarg = 'hanchan'
|
||||
|
||||
def get_success_url(self):
|
||||
"""
|
||||
Return to the HachanList of the event form the deleted hanchan.
|
||||
:return: URL of the EventHanchanList for the event
|
||||
"""
|
||||
return reverse('event-hanchan-list',
|
||||
kwargs={'event': self.object.event.pk})
|
||||
|
||||
@@ -48,32 +50,30 @@ class DeleteHanchan(EventDetailMixin, PermissionRequiredMixin,
|
||||
class HanchanForm(SuccessMessageMixin, EventDetailMixin,
|
||||
PermissionRequiredMixin, generic.UpdateView):
|
||||
"""
|
||||
Ein Formular um neue Hanchans anzulegen, bzw. eine bestehende zu
|
||||
bearbeitsen
|
||||
A Form to add a new or edit an existing Hanchan.
|
||||
"""
|
||||
form_class = forms.HanchanForm
|
||||
model = models.Hanchan
|
||||
permission_required = 'mahjong_ranking.add_hanchan'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = generic.UpdateView.get_context_data(self, **kwargs)
|
||||
context['event'] = self.event
|
||||
return context
|
||||
|
||||
def get_form_class(self):
|
||||
"""
|
||||
Users with edit Persmission will see the AdminForm to confirm
|
||||
unconfirmed Hanchans.
|
||||
Users with hanchan edit persmission can also un-/confirm hanchans.
|
||||
:return: forms.HanchanForm, or forms.HanchanAdminForm
|
||||
"""
|
||||
if self.request.user.has_perm('mahjong_ranking.change_hanchan'):
|
||||
return forms.HanchanAdminForm
|
||||
else:
|
||||
return forms.HanchanForm
|
||||
return forms.HanchanAdminForm if self.request.user.has_perm(
|
||||
'mahjong_ranking.change_hanchan') else forms.HanchanForm
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
"""
|
||||
load the hanchan form the db, or create a new one with the event set.
|
||||
Also sets the event attribute.
|
||||
:param queryset:
|
||||
:return: models.Hanchan object
|
||||
"""
|
||||
if self.kwargs.get('hanchan') and self.request.user.has_perm(
|
||||
'mahjong_ranking.change_hanchan'):
|
||||
hanchan = models.Hanchan.objects.get(id=self.kwargs['hanchan'])
|
||||
hanchan = self.model.objects.get(id=self.kwargs['hanchan'])
|
||||
self.event = hanchan.event
|
||||
elif self.kwargs.get('event'):
|
||||
self.event = models.Event.objects.get(id=self.kwargs['event'])
|
||||
@@ -94,6 +94,11 @@ class HanchanForm(SuccessMessageMixin, EventDetailMixin,
|
||||
return reverse('add-hanchan-form', kwargs={'event': self.event.pk})
|
||||
|
||||
def get_success_message(self, cleaned_data):
|
||||
"""
|
||||
Get the right sucsess message for the django notification subsystem.
|
||||
:param cleaned_data:
|
||||
:return: Sucsess message
|
||||
"""
|
||||
if self.kwargs.get('hanchan'):
|
||||
return _('%s has been updated successfully.') % self.object
|
||||
else:
|
||||
@@ -103,72 +108,32 @@ class HanchanForm(SuccessMessageMixin, EventDetailMixin,
|
||||
|
||||
|
||||
class EventHanchanList(EventDetailMixin, generic.ListView):
|
||||
"""
|
||||
Auflistung aller Hanchan die während der Veranstaltung gespielt wurden.
|
||||
"""
|
||||
"List all hanchans played on a given event."
|
||||
model = models.Hanchan
|
||||
template_name = 'mahjong_ranking/eventhanchan_list.html'
|
||||
|
||||
def get_queryset(self):
|
||||
try:
|
||||
self.event = models.Event.objects.get(pk=self.kwargs['event'])
|
||||
queryset = models.Hanchan.objects.filter(event=self.event)
|
||||
queryset = queryset.order_by('start')
|
||||
return queryset
|
||||
except models.Event.DoesNotExist:
|
||||
raise django.http.Http404(_('Event does not exist'))
|
||||
|
||||
|
||||
class EventRankingList(EventDetailMixin, generic.ListView):
|
||||
"""
|
||||
Anzeige des Eventrankings, daß erstellt wurde falls der Termin als internes
|
||||
Turnier markiert wurde.
|
||||
"""
|
||||
"""Display the event ranking for the given event."""
|
||||
model = models.EventRanking
|
||||
|
||||
def get_queryset(self):
|
||||
try:
|
||||
self.event = models.Event.objects.get(pk=self.kwargs['event'])
|
||||
queryset = models.EventRanking.objects.filter(event=self.event)
|
||||
return queryset.prefetch_related()
|
||||
except models.Event.DoesNotExist:
|
||||
raise django.http.Http404(_('Event does not exist'))
|
||||
|
||||
|
||||
class MahjongMixin(object):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(MahjongMixin, self).get_context_data(**kwargs)
|
||||
try:
|
||||
context['season'] = self.season
|
||||
context['season_start'] = date(year=self.season, month=1, day=1)
|
||||
context['season_end'] = date(year=self.season, month=12, day=31)
|
||||
context['season_list'] = models.SeasonRanking.objects.season_list
|
||||
except AttributeError:
|
||||
pass
|
||||
context[
|
||||
'latest_hanchan_list'] = \
|
||||
models.Hanchan.objects.confirmed_hanchans()[
|
||||
:3]
|
||||
context['latest_event_list'] = Event.objects.upcoming(limit=3)
|
||||
return context
|
||||
|
||||
|
||||
class KyuDanRankingList(MahjongMixin, generic.ListView):
|
||||
"""
|
||||
Anzeige aller Spiele mit ihrem Kyu bzw Dan Grad.
|
||||
"""
|
||||
"""List all Players with an Kyu or Dan score. """
|
||||
default_order = '-score'
|
||||
order_by = ''
|
||||
paginate_by = 25
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.order_by = kyu_dan_order[
|
||||
"""Set the order_by settings, revert to default_order if necessary."""
|
||||
self.order_by = KYU_DAN_ORDER[
|
||||
kwargs.get('order_by', self.default_order)
|
||||
]
|
||||
return generic.ListView.dispatch(self, request, *args, **kwargs)
|
||||
return super(KyuDanRankingList, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = models.KyuDanRanking.objects.all().order_by(*self.order_by)
|
||||
queryset = models.KyuDanRanking.objects.filter(
|
||||
hanchan_count__gt=0).order_by(*self.order_by)
|
||||
return queryset.select_related()
|
||||
|
||||
|
||||
@@ -192,7 +157,6 @@ class PlayerScore(LoginRequiredMixin, generic.ListView):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
user_model = auth.get_user_model()
|
||||
username = kwargs.get('username')
|
||||
try:
|
||||
self.user = user_model.objects.get(
|
||||
username=self.kwargs.get('username'))
|
||||
@@ -200,6 +164,9 @@ class PlayerScore(LoginRequiredMixin, generic.ListView):
|
||||
raise django.http.Http404(
|
||||
_("No user found matching the name {}").format(
|
||||
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)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@@ -218,26 +185,124 @@ class PlayerScore(LoginRequiredMixin, generic.ListView):
|
||||
context['ladder_ranking'] = models.SeasonRanking(user=self.user)
|
||||
return context
|
||||
|
||||
def get_xlsx(self, request, *args, **kwargs):
|
||||
self.object_list = self.get_queryset()
|
||||
response = django.http.HttpResponse(
|
||||
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||
response['Content-Disposition'] = 'attachment; ' \
|
||||
'filename="{xlsx_filename}"'.format(
|
||||
xlsx_filename=self.xlsx_filename)
|
||||
xlxs_workbook = xlsx.Workbook()
|
||||
xlxs_workbook.generate_sheet(
|
||||
title=self.xlsx_filename.split('.')[0],
|
||||
columns_settings=self.xlsx_columns,
|
||||
object_list=self.object_list
|
||||
)
|
||||
xlxs_workbook.save(response)
|
||||
return response
|
||||
|
||||
|
||||
class PlayerDanScore(PlayerScore):
|
||||
template_name = 'mahjong_ranking/player_dan_score.html'
|
||||
|
||||
def get_queryset(self):
|
||||
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):
|
||||
template_name = 'mahjong_ranking/player_invalid_score.html'
|
||||
|
||||
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):
|
||||
template_name = 'mahjong_ranking/player_kyu_score.html'
|
||||
|
||||
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):
|
||||
@@ -259,3 +324,39 @@ class PlayerLadderScore(PlayerScore):
|
||||
season=self.season
|
||||
)
|
||||
return hanchan_list
|
||||
|
||||
@property
|
||||
def xlsx_columns(self):
|
||||
return (
|
||||
{'col': 'A', 'title': 'Beginn', 'attr': 'start',
|
||||
'style': 'Date Time', 'width': 14},
|
||||
{'col': 'B', 'title': 'Termin', 'attr': 'event.name',
|
||||
'style': 'Content', 'width': 16},
|
||||
{'col': 'C', 'title': 'Platzierung', 'attr': 'placement',
|
||||
'style': 'Integer', 'width': 11},
|
||||
{'col': 'D', 'title': 'Spieler 1', 'attr': 'player1.username',
|
||||
'style': 'Content', 'width': 16},
|
||||
{'col': 'E', 'title': 'Punkte', 'attr': 'player1_game_score',
|
||||
'style': 'Integer', 'width': 8},
|
||||
{'col': 'F', 'title': 'Spieler 2', 'attr': 'player2.username',
|
||||
'style': 'Content', 'width': 16},
|
||||
{'col': 'G', 'title': 'Punkte', 'attr': 'player2_game_score',
|
||||
'style': 'Integer', 'width': 8},
|
||||
{'col': 'H', 'title': 'Spieler 3', 'attr': 'player3.username',
|
||||
'style': 'Content', 'width': 16},
|
||||
{'col': 'I', 'title': 'Punkte', 'attr': 'player3_game_score',
|
||||
'style': 'Integer', 'width': 8},
|
||||
{'col': 'J', 'title': 'Spieler 4', 'attr': 'player4.username',
|
||||
'style': 'Content', 'width': 16},
|
||||
{'col': 'K', 'title': 'Punkte', 'attr': 'player4_game_score',
|
||||
'style': 'Integer', 'width': 8},
|
||||
{'col': 'L', 'title': 'Punkte', 'attr': 'game_score',
|
||||
'style': 'Integer', 'width': 8},
|
||||
)
|
||||
|
||||
@property
|
||||
def xlsx_filename(self):
|
||||
return "{username}_ladder_score_{season}.xlsx".format(
|
||||
username=self.user.username,
|
||||
season=self.season
|
||||
)
|
||||
|
||||
@@ -37,19 +37,19 @@ class Migration(migrations.Migration):
|
||||
('season', models.PositiveSmallIntegerField(
|
||||
verbose_name='Saison', editable=False, db_index=True)),
|
||||
('event', models.ForeignKey(
|
||||
related_name='maistargame_set', to='events.Event')),
|
||||
related_name='maistargame_set', to='events.Event', on_delete=models.CASCADE)),
|
||||
('player1', models.ForeignKey(related_name='+',
|
||||
verbose_name='Spieler 1', to=settings.AUTH_USER_MODEL)),
|
||||
verbose_name='Spieler 1', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
||||
('player2', models.ForeignKey(related_name='+',
|
||||
verbose_name='Spieler 2', to=settings.AUTH_USER_MODEL)),
|
||||
verbose_name='Spieler 2', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
||||
('player3', models.ForeignKey(related_name='+',
|
||||
verbose_name='Spieler 3', to=settings.AUTH_USER_MODEL)),
|
||||
verbose_name='Spieler 3', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
||||
('player4', models.ForeignKey(related_name='+',
|
||||
verbose_name='Spieler 4', to=settings.AUTH_USER_MODEL)),
|
||||
verbose_name='Spieler 4', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
||||
('player5', models.ForeignKey(related_name='+',
|
||||
verbose_name='Spieler 5', to=settings.AUTH_USER_MODEL)),
|
||||
verbose_name='Spieler 5', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
||||
('player6', models.ForeignKey(related_name='+',
|
||||
verbose_name='Spieler 6', to=settings.AUTH_USER_MODEL)),
|
||||
verbose_name='Spieler 6', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
@@ -65,7 +65,7 @@ class Migration(migrations.Migration):
|
||||
('games_count', models.PositiveSmallIntegerField(default=0)),
|
||||
('games_good', models.PositiveSmallIntegerField(default=0)),
|
||||
('games_won', models.PositiveSmallIntegerField(default=0)),
|
||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
|
||||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'ordering': ('-season', 'placement', 'avg_placement', '-avg_score'),
|
||||
|
||||
19
src/maistar_ranking/migrations/0006_auto_20171115_0653.py
Normal file
19
src/maistar_ranking/migrations/0006_auto_20171115_0653.py
Normal 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')},
|
||||
),
|
||||
]
|
||||
52
src/maistar_ranking/migrations/0007_auto_20171214_1215.py
Normal file
52
src/maistar_ranking/migrations/0007_auto_20171214_1215.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
import logging
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_delete, post_save
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.dispatch import receiver
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from events.models import Event
|
||||
from . import settings, managers
|
||||
@@ -16,40 +16,47 @@ class Game(models.Model):
|
||||
"""to record a complete game with 6 different players."""
|
||||
|
||||
_player_list = list()
|
||||
event = models.ForeignKey(Event, related_name='maistargame_set')
|
||||
event = models.ForeignKey(Event, on_delete=models.CASCADE,
|
||||
related_name='maistargame_set')
|
||||
comment = models.TextField(_('Comment'), blank=True)
|
||||
player1 = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, verbose_name=_("Player 1"), related_name='+'
|
||||
settings.AUTH_USER_MODEL, on_delete=models.PROTECT,
|
||||
verbose_name=_("Player 1"), related_name='+'
|
||||
)
|
||||
player1_score = models.SmallIntegerField(_("Score"))
|
||||
player1_placement = models.PositiveSmallIntegerField(editable=False)
|
||||
|
||||
player2 = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, verbose_name=_("Player 2"), related_name='+'
|
||||
settings.AUTH_USER_MODEL, on_delete=models.PROTECT,
|
||||
verbose_name=_("Player 2"), related_name='+'
|
||||
)
|
||||
player2_score = models.SmallIntegerField(_("Score"))
|
||||
player2_placement = models.PositiveSmallIntegerField(editable=False)
|
||||
|
||||
player3 = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, verbose_name=_("Player 3"), related_name='+'
|
||||
settings.AUTH_USER_MODEL, on_delete=models.PROTECT,
|
||||
verbose_name=_("Player 3"), related_name='+'
|
||||
)
|
||||
player3_score = models.SmallIntegerField(_("Score"))
|
||||
player3_placement = models.PositiveSmallIntegerField(editable=False)
|
||||
|
||||
player4 = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, verbose_name=_("Player 4"), related_name='+'
|
||||
settings.AUTH_USER_MODEL, on_delete=models.PROTECT,
|
||||
verbose_name=_("Player 4"), related_name='+'
|
||||
)
|
||||
player4_score = models.SmallIntegerField(_("Score"))
|
||||
player4_placement = models.PositiveSmallIntegerField(editable=False)
|
||||
|
||||
player5 = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, verbose_name=_("Player 5"), related_name='+'
|
||||
settings.AUTH_USER_MODEL, on_delete=models.PROTECT,
|
||||
verbose_name=_("Player 5"), related_name='+'
|
||||
)
|
||||
player5_score = models.SmallIntegerField(_("Score"))
|
||||
player5_placement = models.PositiveSmallIntegerField(editable=False)
|
||||
|
||||
player6 = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, verbose_name=_("Player 6"), related_name='+'
|
||||
settings.AUTH_USER_MODEL, on_delete=models.PROTECT,
|
||||
verbose_name=_("Player 6"), related_name='+'
|
||||
)
|
||||
player6_score = models.SmallIntegerField(_("Score"))
|
||||
player6_placement = models.PositiveSmallIntegerField(editable=False)
|
||||
@@ -69,7 +76,6 @@ class Game(models.Model):
|
||||
"""Display rankings by placement, best players first."""
|
||||
ordering = ('-event__start', '-id')
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return _("Mai-Star Game with {0} from {1:%Y-%m-%d}").format(
|
||||
self.player_names, self.event.start
|
||||
@@ -143,7 +149,7 @@ class Game(models.Model):
|
||||
|
||||
class Ranking(models.Model):
|
||||
"""the player scores in the ladder for one season. """
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
|
||||
season = models.PositiveSmallIntegerField(_("Season"))
|
||||
placement = models.PositiveIntegerField(blank=True, null=True)
|
||||
avg_placement = models.PositiveSmallIntegerField(blank=True, null=True)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from datetime import date
|
||||
|
||||
from django.contrib import auth
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.urls import reverse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.views import generic
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import django.core.validators
|
||||
import django.contrib.auth.models
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import models, migrations
|
||||
|
||||
import membership.models
|
||||
import utils
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('auth', '0006_require_contenttypes_0002'),
|
||||
]
|
||||
@@ -21,56 +21,94 @@ class Migration(migrations.Migration):
|
||||
name='Membership',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID',
|
||||
serialize=False, auto_created=True, primary_key=True)),
|
||||
serialize=False, auto_created=True,
|
||||
primary_key=True)),
|
||||
('password', models.CharField(
|
||||
max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(
|
||||
null=True, verbose_name='last login', blank=True)),
|
||||
('is_superuser', models.BooleanField(default=False,
|
||||
help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, max_length=30, validators=[django.core.validators.RegexValidator(
|
||||
'^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.', 'invalid')], help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', unique=True, verbose_name='username')),
|
||||
help_text='Designates that this user has all permissions without explicitly assigning them.',
|
||||
verbose_name='superuser status')),
|
||||
('username', models.CharField(error_messages={
|
||||
'unique': 'A user with that username already exists.'},
|
||||
max_length=30, validators=[
|
||||
django.core.validators.RegexValidator(
|
||||
'^[\\w.@+-]+$',
|
||||
'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.',
|
||||
'invalid')],
|
||||
help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.',
|
||||
unique=True,
|
||||
verbose_name='username')),
|
||||
('first_name', models.CharField(max_length=30,
|
||||
verbose_name='first name', blank=True)),
|
||||
verbose_name='first name',
|
||||
blank=True)),
|
||||
('last_name', models.CharField(max_length=30,
|
||||
verbose_name='last name', blank=True)),
|
||||
verbose_name='last name',
|
||||
blank=True)),
|
||||
('email', models.EmailField(max_length=254,
|
||||
verbose_name='email address', blank=True)),
|
||||
verbose_name='email address',
|
||||
blank=True)),
|
||||
('is_staff', models.BooleanField(default=False,
|
||||
help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
help_text='Designates whether the user can log into this admin site.',
|
||||
verbose_name='staff status')),
|
||||
('is_active', models.BooleanField(
|
||||
default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
default=True,
|
||||
help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.',
|
||||
verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(
|
||||
default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('gender', models.CharField(max_length=1, verbose_name='Geschlecht', choices=[
|
||||
(b'm', 'M\xe4nnlich'), (b'f', 'Weiblich')])),
|
||||
default=django.utils.timezone.now,
|
||||
verbose_name='date joined')),
|
||||
('gender',
|
||||
models.CharField(max_length=1, verbose_name='Geschlecht',
|
||||
choices=[
|
||||
(b'm', 'M\xe4nnlich'),
|
||||
(b'f', 'Weiblich')])),
|
||||
('website', models.URLField(blank=True)),
|
||||
('avatar', models.ImageField(storage=utils.OverwriteStorage(
|
||||
), null=True, upload_to=membership.models.get_upload_path, blank=True)),
|
||||
), null=True, upload_to=membership.models.get_upload_path,
|
||||
blank=True)),
|
||||
('membership', models.BooleanField(default=False,
|
||||
help_text='Ja, ich bin mit den Statuen einverstanden und m\xf6chte Mitglied werden.', verbose_name='Mitgliedschaft')),
|
||||
help_text='Ja, ich bin mit den Statuen einverstanden und m\xf6chte Mitglied werden.',
|
||||
verbose_name='Mitgliedschaft')),
|
||||
('birthday', models.DateField(null=True,
|
||||
verbose_name='Geburtstag', blank=True)),
|
||||
verbose_name='Geburtstag',
|
||||
blank=True)),
|
||||
('telephone', models.CharField(max_length=30,
|
||||
null=True, verbose_name='Telefon', blank=True)),
|
||||
null=True,
|
||||
verbose_name='Telefon',
|
||||
blank=True)),
|
||||
('street_name', models.CharField(max_length=75,
|
||||
null=True, verbose_name='Adresse', blank=True)),
|
||||
null=True,
|
||||
verbose_name='Adresse',
|
||||
blank=True)),
|
||||
('post_code', models.PositiveSmallIntegerField(
|
||||
null=True, verbose_name='Postleitzahl', blank=True)),
|
||||
('city', models.CharField(max_length=75,
|
||||
null=True, verbose_name='Ort', blank=True)),
|
||||
null=True, verbose_name='Ort',
|
||||
blank=True)),
|
||||
('deposit', models.PositiveSmallIntegerField(
|
||||
default=0, editable=False)),
|
||||
('registration_date', models.DateField(auto_now_add=True)),
|
||||
('paid_until', models.DateField(null=True,
|
||||
verbose_name='Bezahlt bis', blank=True)),
|
||||
verbose_name='Bezahlt bis',
|
||||
blank=True)),
|
||||
('confirmed', models.BooleanField(default=False,
|
||||
help_text='Diese Person hat ihre Mitgliedschaft bezahlt', verbose_name='Best\xe4tigt')),
|
||||
help_text='Diese Person hat ihre Mitgliedschaft bezahlt',
|
||||
verbose_name='Best\xe4tigt')),
|
||||
('comment', models.TextField(blank=True)),
|
||||
('groups', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True,
|
||||
help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Permission',
|
||||
blank=True, help_text='Specific permissions for this user.', verbose_name='user permissions')),
|
||||
('groups', models.ManyToManyField(related_query_name='user',
|
||||
related_name='user_set',
|
||||
to='auth.Group', blank=True,
|
||||
help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.',
|
||||
verbose_name='groups')),
|
||||
('user_permissions',
|
||||
models.ManyToManyField(related_query_name='user',
|
||||
related_name='user_set',
|
||||
to='auth.Permission',
|
||||
blank=True,
|
||||
help_text='Specific permissions for this user.',
|
||||
verbose_name='user permissions')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('last_name', 'first_name'),
|
||||
@@ -86,11 +124,13 @@ class Migration(migrations.Migration):
|
||||
name='ActivationRequest',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID',
|
||||
serialize=False, auto_created=True, primary_key=True)),
|
||||
serialize=False, auto_created=True,
|
||||
primary_key=True)),
|
||||
('activation_key', models.CharField(
|
||||
max_length=40, verbose_name='Aktivierungsschl\xfcssel')),
|
||||
('user', models.OneToOneField(
|
||||
verbose_name='Benutzer', to=settings.AUTH_USER_MODEL)),
|
||||
verbose_name='Benutzer', to=settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Ausstehende Aktivierung',
|
||||
|
||||
26
src/membership/migrations/0007_auto_20171115_0653.py
Normal file
26
src/membership/migrations/0007_auto_20171115_0653.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@@ -7,7 +7,7 @@ from os import path
|
||||
|
||||
from django.conf import settings
|
||||
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.utils import timezone
|
||||
from django.utils.translation import ugettext as _
|
||||
@@ -80,6 +80,7 @@ class ActivationRequest(models.Model):
|
||||
"""
|
||||
user = models.OneToOneField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_('user')
|
||||
)
|
||||
activation_key = models.CharField(_('activation key'), max_length=40)
|
||||
@@ -215,6 +216,10 @@ class Membership(AbstractUser):
|
||||
verbose_name = _('Membership')
|
||||
verbose_name_plural = _('Memberships')
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
return " ".join([self.last_name, self.first_name])
|
||||
|
||||
def __str__(self):
|
||||
return self.username
|
||||
|
||||
@@ -226,8 +231,8 @@ class Membership(AbstractUser):
|
||||
"""Save the Useraccount, and add him tho the "Paid Membership" group
|
||||
if he activated the "membership" checkbox and has been validated.
|
||||
|
||||
:param args: passed through the save() method from django
|
||||
:param kwargs: passed through the save() method from django
|
||||
:param args: passed through the save() method from django
|
||||
:param kwargs: passed through the save() method from django
|
||||
"""
|
||||
super(Membership, self).save(*args, **kwargs)
|
||||
if self.confirmed:
|
||||
|
||||
@@ -35,7 +35,10 @@
|
||||
<h3>Mahjong</h3>
|
||||
<ul>
|
||||
{% if kyu_dan_ranking.dan %}
|
||||
<li><strong>{{kyu_dan_ranking.dan}}. Dan: </strong> {{ kyu_dan_ranking.dan_points }} {% trans 'Points' %}</li>
|
||||
<li>
|
||||
<strong>{{kyu_dan_ranking.dan}}. Dan: </strong> {{ kyu_dan_ranking.dan_points }} {% trans 'Points' %}
|
||||
({% trans 'Maximum' %}: {{ kyu_dan_ranking.max_dan_points }})
|
||||
</li>
|
||||
{% elif kyu_dan_ranking.kyu%}
|
||||
<li><strong>{{kyu_dan_ranking.kyu}}. Kyu: </strong> {{ kyu_dan_ranking.kyu_points }} {% trans 'Points' %}</li>
|
||||
{% endif %}
|
||||
|
||||
@@ -7,7 +7,7 @@ from django import http
|
||||
from django.conf import settings
|
||||
from django.contrib import auth, messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.urls import reverse
|
||||
from django.http import Http404
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import ugettext as _
|
||||
@@ -114,7 +114,7 @@ class MembershipDetail(LoginRequiredMixin, generic.DetailView):
|
||||
def get_context_data(self, **kwargs):
|
||||
""" Add the ladder ranking and the Kyu and Dan ranking the user profile.
|
||||
|
||||
:return: array with the context data
|
||||
:return: array with the context data
|
||||
"""
|
||||
context = generic.DetailView.get_context_data(self, **kwargs)
|
||||
try:
|
||||
@@ -139,11 +139,11 @@ class RegisterForm(generic.FormView):
|
||||
|
||||
@method_decorator(csp_update(**RECAPTCHA_CSP))
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
"""Overwrite to add googles recaptcha ressoruces to the
|
||||
"""Overwrite to add googles recaptcha ressoruces to the
|
||||
Content-Security_Policity HTTP Headers.
|
||||
:param request:
|
||||
:param args:
|
||||
:param kwargs:
|
||||
:param request:
|
||||
:param args:
|
||||
:param kwargs:
|
||||
:return: """
|
||||
return super(RegisterForm, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -4,14 +4,14 @@ from django.utils.html import strip_spaces_between_tags
|
||||
|
||||
|
||||
class CompressHtmlMiddleware(object):
|
||||
"""This Middleware compresses strips the spaces between tags, and at the
|
||||
"""This Middleware compresses strips the spaces between tags, and at the
|
||||
beginning and the end of the content."""
|
||||
# TODO: Port to django 1.10 and upward
|
||||
|
||||
def __init__(self, get_response):
|
||||
"""
|
||||
|
||||
:param get_response:
|
||||
:param get_response:
|
||||
"""
|
||||
self.get_response = get_response
|
||||
regex = ">[\s]*<"
|
||||
@@ -19,8 +19,8 @@ class CompressHtmlMiddleware(object):
|
||||
def __call__(self, request):
|
||||
"""
|
||||
|
||||
:param request:
|
||||
:return:
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
|
||||
# Code to be executed for each request before
|
||||
@@ -31,3 +31,15 @@ class CompressHtmlMiddleware(object):
|
||||
response.content = strip_spaces_between_tags(
|
||||
response.content).strip()
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user