From d9e0d5596cc10bfbe498b2dfc6420e32af8faa05 Mon Sep 17 00:00:00 2001
From: Christian Berg
Date: Sun, 19 Nov 2017 16:14:59 +0100
Subject: [PATCH 01/19] added new fields to KyuDanRanking that allow to pick up
the calculation from the last state of the KyuDanRanking. last_hanchan_date:
it contains the start of the latest hanchan content for this players ranking.
wins_in_row: to save the currents wins in a row
Added option to calcuclate rankings until a given datetime.
---
.../management/commands/export_ranking.py | 46 ++++++----
.../management/commands/update_ranking.py | 34 +++----
src/mahjong_ranking/managers.py | 90 ++++++++++++++-----
src/mahjong_ranking/middleware.py | 11 +--
.../migrations/0005_auto_20171115_0653.py | 29 ++++++
src/mahjong_ranking/models.py | 60 ++++++++-----
src/mahjong_ranking/tests.py | 4 +-
7 files changed, 189 insertions(+), 85 deletions(-)
create mode 100644 src/mahjong_ranking/migrations/0005_auto_20171115_0653.py
diff --git a/src/mahjong_ranking/management/commands/export_ranking.py b/src/mahjong_ranking/management/commands/export_ranking.py
index 164ced4..4092565 100644
--- a/src/mahjong_ranking/management/commands/export_ranking.py
+++ b/src/mahjong_ranking/management/commands/export_ranking.py
@@ -1,12 +1,11 @@
"""Export Mahjong Rankings as excel files."""
-from datetime import date
-from operator import itemgetter
-
import openpyxl
from django.core.management.base import BaseCommand
+from django.utils.dateparse import parse_date
from openpyxl.styles import Border
-
+from datetime import date, time, datetime
+from django.utils import timezone
from mahjong_ranking.models import SeasonRanking, KyuDanRanking
THIN_BORDER = openpyxl.styles.Side(style='thin', color="d3d7cf")
@@ -34,6 +33,11 @@ FLOAT_STYLE.font = DEFAULT_STYLE.font
FLOAT_STYLE.border = DEFAULT_STYLE.border
FLOAT_STYLE.number_format = '#,##0.00'
+DATE_STYLE = openpyxl.styles.NamedStyle(name='date')
+DATE_STYLE.font = DEFAULT_STYLE.font
+DATE_STYLE.border = DEFAULT_STYLE.border
+DATE_STYLE.number_format = 'dd.mm.yyyy'
+
def geneate_excel():
"""Generate an excel .xlsx spreadsheet from json data of the kyu/dan
@@ -45,8 +49,9 @@ def geneate_excel():
workbook.add_named_style(DEFAULT_STYLE)
workbook.add_named_style(INT_STYLE)
workbook.add_named_style(FLOAT_STYLE)
+ workbook.add_named_style(DATE_STYLE)
+
for sheet in workbook.worksheets:
- print(sheet)
workbook.remove(sheet)
return workbook
@@ -55,6 +60,8 @@ def generate_sheet(workbook, title, columns_settings, json_data):
row = 1
ws = workbook.create_sheet()
ws.title = title
+ ws.syncHorizontal = True
+ ws.filterMode = True
# setup print orientation
ws.page_setup.orientation = ws.ORIENTATION_PORTRAIT
@@ -90,10 +97,10 @@ def generate_sheet(workbook, title, columns_settings, json_data):
ws.column_dimensions[settings['col']].width = settings['width']
-def export_season_rankings(workbook):
- json_data = sorted(SeasonRanking.objects.json_data(),
- key=itemgetter('placement'))
- title = "Mahjong Ladder - {}".format(date.today().year)
+def export_season_rankings(workbook, until):
+ SeasonRanking.objects.update(until=until)
+ json_data = SeasonRanking.objects.json_data()
+ title = "Mahjong Ladder - {}".format(until.year)
columns_settings = (
{'col': 'A', 'title': 'Rang', 'attr': 'placement', 'style': 'int',
'width': 8},
@@ -111,7 +118,6 @@ def export_season_rankings(workbook):
{'col': 'G', 'title': 'Gewonnen', 'attr': 'won_hanchans',
'style': 'int', 'width': 10},
)
-
generate_sheet(
workbook=workbook,
title=title,
@@ -119,7 +125,8 @@ def export_season_rankings(workbook):
json_data=json_data)
-def export_kyu_dan_rankings(workbook):
+def export_kyu_dan_rankings(workbook, until):
+ KyuDanRanking.objects.update(until=until)
json_data = KyuDanRanking.objects.json_data()
title = "Kyū & Dan Rankings"
columns_settings = (
@@ -136,7 +143,9 @@ def export_kyu_dan_rankings(workbook):
{'col': 'F', 'title': 'Gut', 'attr': 'good_hanchans',
'style': 'int', 'width': 5},
{'col': 'G', 'title': 'Gewonnen', 'attr': 'won_hanchans',
- 'style': 'int', 'width': 10},
+ 'style': 'int', 'width': 8},
+ {'col': 'H', 'title': 'letzte Hanchan', 'attr': 'last_hanchan_date',
+ 'style': 'date', 'width': 16},
)
generate_sheet(
workbook=workbook,
@@ -148,11 +157,16 @@ def export_kyu_dan_rankings(workbook):
class Command(BaseCommand):
"""Exports the SeasonRankings"""
+ def add_arguments(self, parser):
+ parser.add_argument('--until', nargs='?', type=parse_date)
+
def handle(self, *args, **options):
"""Exports the current ladder ranking in a spreadsheet.
This is useful as a backup in form of a hardcopy."""
+ until = timezone.make_aware(
+ datetime.combine(options['until'] or date.today(),
+ time(23, 59, 59)))
workbook = geneate_excel()
- export_season_rankings(workbook)
- export_kyu_dan_rankings(workbook)
- workbook.save('sample.x')
- workbook.save('mahjong_rankings_{}.xlsx'.format(str(date.today())))
+ export_season_rankings(workbook, until=until)
+ export_kyu_dan_rankings(workbook, until=until)
+ workbook.save('mahjong_rankings_{:%Y-%m-%d}.xlsx'.format(until))
diff --git a/src/mahjong_ranking/management/commands/update_ranking.py b/src/mahjong_ranking/management/commands/update_ranking.py
index 692a8e1..d7a8fd7 100644
--- a/src/mahjong_ranking/management/commands/update_ranking.py
+++ b/src/mahjong_ranking/management/commands/update_ranking.py
@@ -5,27 +5,29 @@ Recalculate Mahjong Rankings...
"""
from django.core.management.base import BaseCommand
-
-from mahjong_ranking import LOGGER
+from datetime import date, datetime, time
from mahjong_ranking import models
-
+from django.utils.dateparse import parse_date
+from django.utils import timezone
class Command(BaseCommand):
""" Recalculate all Kyu/Dan Rankings """
help = "Recalculate all Kyu/Dan Rankings"
+ def add_arguments(self, parser):
+ parser.add_argument('--since', nargs='?', type=parse_date)
+ parser.add_argument('--until', nargs='?', type=parse_date)
+ parser.add_argument('--forcerecalc', action='store_true')
+
def handle(self, *args, **options):
- old_attr = {'dan': None, 'dan_points': None,
- 'kyu': None, 'kyu_points': None, 'won_hanchans': None,
- 'good_hanchans': None, 'hanchan_count': None}
- for ranking in models.KyuDanRanking.objects.all():
- old_attr = {attr: getattr(ranking, attr) for attr in old_attr.keys()}
- ranking.recalculate()
- for attr, old_value in old_attr.items():
- if getattr(ranking, attr) != old_value:
- LOGGER.warning(
- "%(user)s recalc shows differences in %(attr)s! old: %(old)d, new: %(new)d",
- {'user': ranking.user, 'attr': attr,
- 'old': old_value, 'new': getattr(ranking, attr)}
- )
+ since = options.get('since', None)
+ until = options.get('until', None)
+ force_recalc = options.get('forecerecalc', False)
+ if isinstance(since, date):
+ since = datetime.combine(since, time(0, 0, 0))
+ since = timezone.make_aware(since)
+ if isinstance(until, date):
+ until = datetime.combine(until, time(23, 59, 59))
+ until = timezone.make_aware(until)
+ models.KyuDanRanking.objects.update(since=since, until=until, force_recalc=force_recalc)
\ No newline at end of file
diff --git a/src/mahjong_ranking/managers.py b/src/mahjong_ranking/managers.py
index aa0421a..43b3a33 100644
--- a/src/mahjong_ranking/managers.py
+++ b/src/mahjong_ranking/managers.py
@@ -1,7 +1,8 @@
"""ObjectManagers for the Django Models used in the Mahjong-Ranking."""
from datetime import date
-
+from . import LOGGER
from django.db import models
+from django.conf import settings
class HanchanManager(models.Manager):
@@ -12,7 +13,7 @@ class HanchanManager(models.Manager):
"""
use_for_related_fields = True
- def confirmed_hanchans(self, user=None, **filter_args):
+ def confirmed_hanchans(self, user=None, until=None, **filter_args):
""" Return all valid and confirmed Hanchans.
:param user: Only return Hanchans where this user participated.
@@ -20,9 +21,12 @@ class HanchanManager(models.Manager):
: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 until:
+ hanchans = hanchans.filter(start__lte=until)
+ return hanchans
def dan_hanchans(self, user, **filter_args):
""" Return all Hanchans where a specific user has participated and had
@@ -39,7 +43,7 @@ class HanchanManager(models.Manager):
models.Q(player4=user, player4_dan_points__isnull=False)
).filter(confirmed=True, **filter_args)
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):
@@ -57,20 +61,23 @@ class HanchanManager(models.Manager):
models.Q(player4=user, player4_kyu_points__isnull=False)
).filter(confirmed=True, **filter_args)
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_hanchans(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.
@@ -82,12 +89,13 @@ 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):
@@ -158,8 +166,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)):
+ 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 +199,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 +213,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, 'new': getattr(ranking, attr)}
+ )
diff --git a/src/mahjong_ranking/middleware.py b/src/mahjong_ranking/middleware.py
index 299edc1..c13853b 100644
--- a/src/mahjong_ranking/middleware.py
+++ b/src/mahjong_ranking/middleware.py
@@ -35,7 +35,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 +58,5 @@ class DenormalizationUpdateMiddleware(object): # Ignore PyLintBear (R0903)
for season in season_queue:
LOGGER.info(u'Recalculate placements for Season %d', season)
- season_rankings = models.SeasonRanking.objects.filter(
- season=season, hanchan_count__gt=MIN_HANCHANS_FOR_LADDER
- ).order_by('avg_placement', '-avg_score')
- placement = 1
- for ranking in season_rankings:
- ranking.placement = placement
- ranking.save(force_update=True)
- placement += 1
+ models.SeasonRanking.objects.update(season=season)
return response
diff --git a/src/mahjong_ranking/migrations/0005_auto_20171115_0653.py b/src/mahjong_ranking/migrations/0005_auto_20171115_0653.py
new file mode 100644
index 0000000..50b21a8
--- /dev/null
+++ b/src/mahjong_ranking/migrations/0005_auto_20171115_0653.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.7 on 2017-11-15 05:53
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('mahjong_ranking', '0004_auto_20170218_1947'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='kyudanranking',
+ options={'ordering': ('-dan_points', 'dan', '-kyu_points'), 'verbose_name': 'Kyū/Dan Wertung', 'verbose_name_plural': 'Kyū/Dan Wertungen'},
+ ),
+ migrations.AddField(
+ model_name='kyudanranking',
+ name='last_hanchan_date',
+ field=models.DateTimeField(blank=True, null=True),
+ ),
+ migrations.AddField(
+ model_name='kyudanranking',
+ name='wins_in_a_row',
+ field=models.PositiveIntegerField(default=0),
+ ),
+ ]
diff --git a/src/mahjong_ranking/models.py b/src/mahjong_ranking/models.py
index 191a727..832b5d4 100644
--- a/src/mahjong_ranking/models.py
+++ b/src/mahjong_ranking/models.py
@@ -4,7 +4,7 @@
# werden dürfen.
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
@@ -345,10 +345,10 @@ class KyuDanRanking(models.Model):
legacy_hanchan_count = models.PositiveIntegerField(default=0)
legacy_dan_points = models.PositiveIntegerField(default=0)
legacy_kyu_points = models.PositiveIntegerField(default=0)
- wins_in_a_row = 0
+ wins_in_a_row = models.PositiveIntegerField(default=0)
+ last_hanchan_date = models.DateTimeField(blank=True, null=True)
objects = managers.KyuDanRankingManager()
-
class Meta(object):
ordering = ('-dan_points', 'dan', '-kyu_points')
verbose_name = _(u'Kyū/Dan Ranking')
@@ -436,28 +436,43 @@ 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_hanchans(user=self.user)
+ valid_hanchans = valid_hanchans.order_by('start')
+ if since and self.last_hanchan_date and since < self.last_hanchan_date:
+ force_recalc = True
+ if until and self.last_hanchan_date and until < self.last_hanchan_date:
+ force_recalc = True
+ if force_recalc:
+ # Setze alles auf die legacy Werte und berechne alles von neuem.
+ self.dan = None
+ self.dan_points = self.legacy_dan_points or 0
+ self.kyu = None
+ self.kyu_points = self.legacy_kyu_points or 0
+ self.hanchan_count = self.legacy_hanchan_count or 0
+ self.good_hanchans = 0
+ self.won_hanchans = 0
+ self.update_rank()
+ self.last_hanchan_date = None
+ if self.legacy_date:
+ since = timezone.make_aware(
+ datetime.combine(self.legacy_date, time(0, 0, 0)))
+ else:
+ since = None
+ else:
+ since = self.last_hanchan_date or self.legacy_date
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)
+ if since:
+ valid_hanchans = valid_hanchans.filter(start__gt=since)
+ if until:
+ valid_hanchans = valid_hanchans.filter(start__lte=until)
""" TODO: Hanchan Punkte nur neu berechnen wenn sie nach hachan_start
lagen. Es müssen aber alle durch die Schleife rennen, damit die Punkte
@@ -465,7 +480,7 @@ class KyuDanRanking(models.Model):
self.hanchan_count += valid_hanchans.count()
for hanchan in valid_hanchans:
hanchan.get_playerdata(self.user)
- if hanchan_start and hanchan_start < hanchan.start:
+ if since and since < hanchan.start:
self.dan_points += hanchan.dan_points or 0
self.kyu_points += hanchan.kyu_points or 0
self.update_rank()
@@ -482,6 +497,7 @@ class KyuDanRanking(models.Model):
hanchan.save(recalculate=False)
self.won_hanchans += 1 if hanchan.placement == 1 else 0
self.good_hanchans += 1 if hanchan.placement == 2 else 0
+ self.last_hanchan_date = hanchan.start
LOGGER.debug(
'id: %(id)d, start: %(start)s, placement: %(placement)d, '
'score: %(score)d, kyu points: %(kyu_points)d, dan points: '
@@ -593,9 +609,9 @@ class SeasonRanking(models.Model):
def get_absolute_url(self):
return reverse('player-ladder-score', args=[self.user.username])
- def recalculate(self):
+ def recalculate(self, until=None):
season_hanchans = Hanchan.objects.season_hanchans(
- user=self.user, season=self.season)
+ user=self.user, season=self.season, until=until)
sum_placement = 0
sum_score = 0
self.placement = None
diff --git a/src/mahjong_ranking/tests.py b/src/mahjong_ranking/tests.py
index 63f1a86..22c2d56 100644
--- a/src/mahjong_ranking/tests.py
+++ b/src/mahjong_ranking/tests.py
@@ -34,7 +34,7 @@ class KyuDanTest(TestCase):
for ranking in KyuDanRanking.objects.all():
original = {a: getattr(ranking, a) for a in self.equal_attrs}
- ranking.recalculate()
+ ranking.calculate()
for attr in self.equal_attrs:
self.assertEqual(
original[attr],
@@ -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],
From c0c48f950ace8e727918096898412791445037a4 Mon Sep 17 00:00:00 2001
From: Christian Berg
Date: Sun, 19 Nov 2017 16:55:10 +0100
Subject: [PATCH 02/19] new command: resetdanranking YYYY-MM-DD, sets every dan
player to 1st dan with zero dan_points at the given date.
---
.../management/commands/resetdanranking.py | 32 +++++++++++++++++++
1 file changed, 32 insertions(+)
create mode 100644 src/mahjong_ranking/management/commands/resetdanranking.py
diff --git a/src/mahjong_ranking/management/commands/resetdanranking.py b/src/mahjong_ranking/management/commands/resetdanranking.py
new file mode 100644
index 0000000..2ead5e5
--- /dev/null
+++ b/src/mahjong_ranking/management/commands/resetdanranking.py
@@ -0,0 +1,32 @@
+"""
+Rest all dan points to 0 at a given date.
+"""
+
+from django.core.management.base import BaseCommand
+from datetime import date, datetime, time
+from mahjong_ranking import models
+from django.utils.dateparse import parse_date
+from django.utils import timezone
+
+class Command(BaseCommand):
+ """ Recalculate all Kyu/Dan Rankings """
+
+ help = "reset every dan player to 1st dan with 0 points."
+
+ def add_arguments(self, parser):
+ parser.add_argument('reset_date', type=parse_date)
+
+ def handle(self, *args, **options):
+ reset_date = timezone.make_aware(datetime.combine(options.get('reset_date'), time(23, 59, 59)))
+ # models.KyuDanRanking.objects.update(until=reset_date, force_recalc=True)
+ dan_rankigns = models.KyuDanRanking.objects.filter(dan__isnull=False)
+ for ranking in dan_rankigns:
+ ranking.dan = 1
+ ranking.dan_points = 0
+ ranking.legacy_date = reset_date.date()
+ ranking.legacy_hanchan_count = ranking.hanchan_count
+ ranking.legacy_dan_points = ranking.dan_points
+ ranking.legacy_kyu_points = ranking.kyu_points
+ ranking.save()
+
+
From 35a51091bf3eac051bb7540670864870def257b5 Mon Sep 17 00:00:00 2001
From: Christian Berg
Date: Mon, 20 Nov 2017 07:33:54 +0100
Subject: [PATCH 03/19] added a since parameter to the hanchan queries to
return only hanchans since the give date and time
---
src/mahjong_ranking/managers.py | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/src/mahjong_ranking/managers.py b/src/mahjong_ranking/managers.py
index 43b3a33..58cc202 100644
--- a/src/mahjong_ranking/managers.py
+++ b/src/mahjong_ranking/managers.py
@@ -13,7 +13,7 @@ class HanchanManager(models.Manager):
"""
use_for_related_fields = True
- def confirmed_hanchans(self, user=None, until=None, **filter_args):
+ def confirmed_hanchans(self, user=None, since=None, until=None, **filter_args):
""" Return all valid and confirmed Hanchans.
:param user: Only return Hanchans where this user participated.
@@ -24,11 +24,13 @@ class HanchanManager(models.Manager):
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.
@@ -42,11 +44,13 @@ 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]
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.
@@ -60,6 +64,8 @@ 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]
return queryset
From bb110da5a2dc9a38cc9cd6726e29617488f3139e Mon Sep 17 00:00:00 2001
From: Christian Berg
Date: Mon, 20 Nov 2017 07:41:04 +0100
Subject: [PATCH 04/19] Updated docstrings for new since and until kwargs
---
src/mahjong_ranking/managers.py | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/mahjong_ranking/managers.py b/src/mahjong_ranking/managers.py
index 58cc202..1ed70f8 100644
--- a/src/mahjong_ranking/managers.py
+++ b/src/mahjong_ranking/managers.py
@@ -16,7 +16,9 @@ class HanchanManager(models.Manager):
def confirmed_hanchans(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
"""
@@ -35,6 +37,7 @@ class HanchanManager(models.Manager):
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
"""
@@ -55,6 +58,7 @@ class HanchanManager(models.Manager):
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
"""
@@ -87,7 +91,7 @@ class HanchanManager(models.Manager):
"""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
"""
From d33e5fc8c6276804d5a8a3a18db4f99c74d2c427 Mon Sep 17 00:00:00 2001
From: Christian Berg
Date: Mon, 20 Nov 2017 07:42:44 +0100
Subject: [PATCH 05/19] fixed import of MIN_HANCHANS_FOR_LADDER that moved to
settings
---
src/mahjong_ranking/middleware.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/mahjong_ranking/middleware.py b/src/mahjong_ranking/middleware.py
index c13853b..2b71256 100644
--- a/src/mahjong_ranking/middleware.py
+++ b/src/mahjong_ranking/middleware.py
@@ -1,8 +1,7 @@
"""Middleware to defer slow denormalization at the end of a request."""
from django.core.cache import cache
-
from mahjong_ranking import models
-from . import LOGGER, MIN_HANCHANS_FOR_LADDER
+from . import LOGGER
class DenormalizationUpdateMiddleware(object): # Ignore PyLintBear (R0903)
From 92470514c4bb349e0d7cb3cab50fd89bc9b3b66b Mon Sep 17 00:00:00 2001
From: Christian Berg
Date: Mon, 20 Nov 2017 07:47:47 +0100
Subject: [PATCH 06/19] Changed PlayerDanScore to only list non-legacy hanchans
---
src/mahjong_ranking/models.py | 1 +
src/mahjong_ranking/views.py | 3 ++-
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/mahjong_ranking/models.py b/src/mahjong_ranking/models.py
index 832b5d4..2335ca8 100644
--- a/src/mahjong_ranking/models.py
+++ b/src/mahjong_ranking/models.py
@@ -510,6 +510,7 @@ class KyuDanRanking(models.Model):
)
self.save(force_update=True)
+
def update_hanchan_points(self, hanchan):
"""
Berechne die Kyu bzw. Dan Punkte für eine Hanchan neu.
diff --git a/src/mahjong_ranking/views.py b/src/mahjong_ranking/views.py
index 44c2dcd..2c121cc 100644
--- a/src/mahjong_ranking/views.py
+++ b/src/mahjong_ranking/views.py
@@ -223,7 +223,8 @@ class PlayerDanScore(PlayerScore):
template_name = 'mahjong_ranking/player_dan_score.html'
def get_queryset(self):
- return models.Hanchan.objects.dan_hanchans(user=self.user)
+ kyu_dan_ranking = models.KyuDanRanking.objects.get(user=self.user)
+ return models.Hanchan.objects.dan_hanchans(user=self.user, since=kyu_dan_ranking.legacy_date)
class PlayerInvalidScore(PlayerScore):
From 68c484afc9ab26efd3251bfef485a681bba1c530 Mon Sep 17 00:00:00 2001
From: Christian Berg
Date: Thu, 23 Nov 2017 14:15:12 +0100
Subject: [PATCH 07/19] add a latest method to query the latest x events
---
src/events/managers.py | 4 ++
src/events/mixins.py | 16 ++++-
src/events/views.py | 3 +-
src/mahjong_ranking/managers.py | 6 +-
src/mahjong_ranking/mixins.py | 18 ++++++
src/mahjong_ranking/models.py | 4 +-
src/mahjong_ranking/tests.py | 4 +-
src/mahjong_ranking/views.py | 103 +++++++++++---------------------
8 files changed, 81 insertions(+), 77 deletions(-)
create mode 100644 src/mahjong_ranking/mixins.py
diff --git a/src/events/managers.py b/src/events/managers.py
index d845727..ceeda5a 100644
--- a/src/events/managers.py
+++ b/src/events/managers.py
@@ -31,6 +31,10 @@ class EventManager(models.Manager):
"""Returns all past events."""
return self.filter(start__lt=now())
+ def latest(self, limit=None):
+ result = self.filter(start__lt=now()).order_by('-start', '-end')
+ return result[0:limit] if limit else result
+
def upcoming(self, limit=None):
"""Returns the next 'limit' upcoming events.
diff --git a/src/events/mixins.py b/src/events/mixins.py
index cb8f372..9b9f915 100644
--- a/src/events/mixins.py
+++ b/src/events/mixins.py
@@ -1,4 +1,6 @@
"""Mixins for Events."""
+from django.http import Http404
+
from . import models
@@ -9,7 +11,6 @@ class EventArchiveMixin(object):
date_field = 'start'
make_object_list = True
model = models.Event
- ordering = ('start', 'end')
paginate_by = 15
template_name = 'events/event_archive.html'
@@ -40,3 +41,16 @@ class EventDetailMixin(object):
elif hasattr(self, 'object') and hasattr(self.object, 'event'):
context['event'] = self.object.event
return context
+
+ def get_queryset(self):
+ """set event attribute from the URL kwarg event and
+ load all related objects from the set model.
+
+ :return: a django QuerySets
+ """
+ try:
+ self.event = models.Event.objects.get(pk=self.kwargs['event'])
+ queryset = self.model.objects.filter(event=self.event)
+ except models.Event.DoesNotExist:
+ raise Http404(_('Event does not exist'))
+ return queryset.prefetch_related()
diff --git a/src/events/views.py b/src/events/views.py
index 1dfa8a0..e92bd3a 100644
--- a/src/events/views.py
+++ b/src/events/views.py
@@ -30,7 +30,6 @@ class DeleteEventPhoto(PermissionRequiredMixin, mixins.EventDetailMixin,
class EventArchiveIndex(mixins.EventArchiveMixin, generic.ArchiveIndexView):
"""Index of the event archive, displays the upcoming events first."""
allow_empty = True
- ordering = ('-start', '-end')
class EventArchiveMonth(mixins.EventArchiveMixin, generic.MonthArchiveView):
@@ -73,7 +72,7 @@ class EventForm(PermissionRequiredMixin, mixins.EventDetailMixin,
if self.kwargs.get('pk') else models.Event()
-class EventGallery(mixins.EventDetailMixin, generic.ListView):
+class EventGallery(generic.ListView):
"""Display a overview of all event photo albums."""
template_name = 'events/photo_gallery.html'
queryset = models.Event.objects.filter(
diff --git a/src/mahjong_ranking/managers.py b/src/mahjong_ranking/managers.py
index 1ed70f8..08560b4 100644
--- a/src/mahjong_ranking/managers.py
+++ b/src/mahjong_ranking/managers.py
@@ -13,7 +13,7 @@ class HanchanManager(models.Manager):
"""
use_for_related_fields = True
- def confirmed_hanchans(self, user=None, since=None, until=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.
@@ -85,7 +85,7 @@ class HanchanManager(models.Manager):
season = season or until.year
except AttributeError:
season = date.today().year
- return self.confirmed_hanchans(user=user, season=season, until=until)
+ return self.confirmed(user=user, season=season, until=until)
def user_hanchans(self, user, since=None, until=None, **filter_args):
"""Return all Hanchans where a specific user has participated.
@@ -108,7 +108,7 @@ class HanchanManager(models.Manager):
[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.
diff --git a/src/mahjong_ranking/mixins.py b/src/mahjong_ranking/mixins.py
new file mode 100644
index 0000000..f621d8a
--- /dev/null
+++ b/src/mahjong_ranking/mixins.py
@@ -0,0 +1,18 @@
+from datetime import date
+from .models import Hanchan, SeasonRanking
+from events.models import Event
+
+
+class MahjongMixin(object):
+ def get_context_data(self, **kwargs):
+ context = super(MahjongMixin, self).get_context_data(**kwargs)
+ try:
+ context['season'] = self.season
+ context['season_start'] = date(year=self.season, month=1, day=1)
+ context['season_end'] = date(year=self.season, month=12, day=31)
+ context['season_list'] = SeasonRanking.objects.season_list
+ except AttributeError:
+ pass
+ context['latest_hanchan_list'] = Hanchan.objects.confirmed()[:3]
+ context['latest_event_list'] = Event.objects.latest(limit=3)
+ return context
diff --git a/src/mahjong_ranking/models.py b/src/mahjong_ranking/models.py
index 2335ca8..e31bff7 100644
--- a/src/mahjong_ranking/models.py
+++ b/src/mahjong_ranking/models.py
@@ -56,7 +56,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
)
@@ -441,7 +441,7 @@ class KyuDanRanking(models.Model):
Fetches all valid Hanchans from this Player and recalculates his
Kyu/Dan Ranking.
"""
- valid_hanchans = Hanchan.objects.confirmed_hanchans(user=self.user)
+ 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
diff --git a/src/mahjong_ranking/tests.py b/src/mahjong_ranking/tests.py
index 22c2d56..ca85a6c 100644
--- a/src/mahjong_ranking/tests.py
+++ b/src/mahjong_ranking/tests.py
@@ -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
)
@@ -86,7 +86,7 @@ class KyuDanTest(TestCase):
'dan_points': ranking.legacy_dan_points or 0,
'kyu_points': ranking.legacy_kyu_points or 0
}
- confirmed_hanchans = Hanchan.objects.confirmed_hanchans(
+ confirmed_hanchans = Hanchan.objects.confirmed(
user=ranking.user,
since=ranking.legacy_date
)
diff --git a/src/mahjong_ranking/views.py b/src/mahjong_ranking/views.py
index 2c121cc..5394070 100644
--- a/src/mahjong_ranking/views.py
+++ b/src/mahjong_ranking/views.py
@@ -11,11 +11,11 @@ from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from django.views import generic
-from events.models import Event
from events.mixins import EventDetailMixin
from . import forms, models
+from .mixins import MahjongMixin
-kyu_dan_order = {
+KYU_DAN_ORDER = { # map sort URL args to Django ORM order_by args
'+full_name': ('user__last_name', 'user__first_name'),
'-full_name': ('-user__last_name', '-user__first_name'),
'+hanchan_count': ('hanchan_count',),
@@ -31,16 +31,17 @@ kyu_dan_order = {
class DeleteHanchan(EventDetailMixin, PermissionRequiredMixin,
generic.DeleteView):
- """
- Fragt zuerst nach, ob die Hanchan wirklich gelöscht werden soll.
- Wir die Frage mit "Ja" beantwortet, wird die die Hanchan gelöscht.
- """
+ """Deletes a Hanchan if confimration has been answerd with 'yes'."""
form_class = forms.HanchanForm
model = models.Hanchan
permission_required = 'mahjong_ranking.delete_hanchan'
pk_url_kwarg = 'hanchan'
def get_success_url(self):
+ """
+ Return to the HachanList of the event form the deleted hanchan.
+ :return: URL of the EventHanchanList for the event
+ """
return reverse('event-hanchan-list',
kwargs={'event': self.object.event.pk})
@@ -48,32 +49,30 @@ class DeleteHanchan(EventDetailMixin, PermissionRequiredMixin,
class HanchanForm(SuccessMessageMixin, EventDetailMixin,
PermissionRequiredMixin, generic.UpdateView):
"""
- Ein Formular um neue Hanchans anzulegen, bzw. eine bestehende zu
- bearbeitsen
+ A Form to add a new or edit an existing Hanchan.
"""
form_class = forms.HanchanForm
model = models.Hanchan
permission_required = 'mahjong_ranking.add_hanchan'
- def get_context_data(self, **kwargs):
- context = generic.UpdateView.get_context_data(self, **kwargs)
- context['event'] = self.event
- return context
-
def get_form_class(self):
"""
- Users with edit Persmission will see the AdminForm to confirm
- unconfirmed Hanchans.
+ Users with hanchan edit persmission can also un-/confirm hanchans.
+ :return: forms.HanchanForm, or forms.HanchanAdminForm
"""
- if self.request.user.has_perm('mahjong_ranking.change_hanchan'):
- return forms.HanchanAdminForm
- else:
- return forms.HanchanForm
+ return forms.HanchanAdminForm if self.request.user.has_perm(
+ 'mahjong_ranking.change_hanchan') else forms.HanchanForm
def get_object(self, queryset=None):
+ """
+ load the hanchan form the db, or create a new one with the event set.
+ Also sets the event attribute.
+ :param queryset:
+ :return: models.Hanchan object
+ """
if self.kwargs.get('hanchan') and self.request.user.has_perm(
'mahjong_ranking.change_hanchan'):
- hanchan = models.Hanchan.objects.get(id=self.kwargs['hanchan'])
+ hanchan = self.model.objects.get(id=self.kwargs['hanchan'])
self.event = hanchan.event
elif self.kwargs.get('event'):
self.event = models.Event.objects.get(id=self.kwargs['event'])
@@ -94,6 +93,11 @@ class HanchanForm(SuccessMessageMixin, EventDetailMixin,
return reverse('add-hanchan-form', kwargs={'event': self.event.pk})
def get_success_message(self, cleaned_data):
+ """
+ Get the right sucsess message for the django notification subsystem.
+ :param cleaned_data:
+ :return: Sucsess message
+ """
if self.kwargs.get('hanchan'):
return _('%s has been updated successfully.') % self.object
else:
@@ -103,72 +107,36 @@ class HanchanForm(SuccessMessageMixin, EventDetailMixin,
class EventHanchanList(EventDetailMixin, generic.ListView):
- """
- Auflistung aller Hanchan die während der Veranstaltung gespielt wurden.
- """
+ "List all hanchans played on a given event."
model = models.Hanchan
template_name = 'mahjong_ranking/eventhanchan_list.html'
- def get_queryset(self):
- try:
- self.event = models.Event.objects.get(pk=self.kwargs['event'])
- queryset = models.Hanchan.objects.filter(event=self.event)
- queryset = queryset.order_by('start')
- return queryset
- except models.Event.DoesNotExist:
- raise django.http.Http404(_('Event does not exist'))
-
class EventRankingList(EventDetailMixin, generic.ListView):
- """
- Anzeige des Eventrankings, daß erstellt wurde falls der Termin als internes
- Turnier markiert wurde.
- """
+ """Display the event ranking for the given event."""
model = models.EventRanking
- def get_queryset(self):
- try:
- self.event = models.Event.objects.get(pk=self.kwargs['event'])
- queryset = models.EventRanking.objects.filter(event=self.event)
- return queryset.prefetch_related()
- except models.Event.DoesNotExist:
- raise django.http.Http404(_('Event does not exist'))
-class MahjongMixin(object):
- def get_context_data(self, **kwargs):
- context = super(MahjongMixin, self).get_context_data(**kwargs)
- try:
- context['season'] = self.season
- context['season_start'] = date(year=self.season, month=1, day=1)
- context['season_end'] = date(year=self.season, month=12, day=31)
- context['season_list'] = models.SeasonRanking.objects.season_list
- except AttributeError:
- pass
- context[
- 'latest_hanchan_list'] = \
- models.Hanchan.objects.confirmed_hanchans()[
- :3]
- context['latest_event_list'] = Event.objects.upcoming(limit=3)
- return context
class KyuDanRankingList(MahjongMixin, generic.ListView):
- """
- Anzeige aller Spiele mit ihrem Kyu bzw Dan Grad.
- """
+ """List all Players with an Kyu or Dan score. """
default_order = '-score'
order_by = ''
paginate_by = 25
def dispatch(self, request, *args, **kwargs):
- self.order_by = kyu_dan_order[
+ """Set the order_by settings, revert to default_order if necessary."""
+ self.order_by = KYU_DAN_ORDER[
kwargs.get('order_by', self.default_order)
]
- return generic.ListView.dispatch(self, request, *args, **kwargs)
+ return super(KyuDanRankingList, self).dispatch(request, *args, **kwargs)
+
def get_queryset(self):
- queryset = models.KyuDanRanking.objects.all().order_by(*self.order_by)
+ queryset = models.KyuDanRanking.objects.filter(
+ hanchan_count__gt=0).order_by(*self.order_by)
return queryset.select_related()
@@ -224,14 +192,15 @@ class PlayerDanScore(PlayerScore):
def get_queryset(self):
kyu_dan_ranking = models.KyuDanRanking.objects.get(user=self.user)
- return models.Hanchan.objects.dan_hanchans(user=self.user, since=kyu_dan_ranking.legacy_date)
+ return models.Hanchan.objects.dan_hanchans(user=self.user,
+ since=kyu_dan_ranking.legacy_date)
class PlayerInvalidScore(PlayerScore):
template_name = 'mahjong_ranking/player_invalid_score.html'
def get_queryset(self):
- return models.Hanchan.objects.unconfirmed_hanchans(user=self.user)
+ return models.Hanchan.objects.unconfirmed(user=self.user)
class PlayerKyuScore(PlayerScore):
From 3a611ca9dab5a6a54dfd4e77f7f9a1652639fca0 Mon Sep 17 00:00:00 2001
From: Christian Berg
Date: Thu, 23 Nov 2017 14:15:36 +0100
Subject: [PATCH 08/19] add a latest method to query the latest x events
---
.../migrations/0006_auto_20171115_0653.py | 91 +++++++++++++++++++
.../migrations/0008_auto_20171115_0653.py | 20 ++++
.../migrations/0006_auto_20171115_0653.py | 19 ++++
.../migrations/0007_auto_20171115_0653.py | 26 ++++++
4 files changed, 156 insertions(+)
create mode 100644 src/content/migrations/0006_auto_20171115_0653.py
create mode 100644 src/events/migrations/0008_auto_20171115_0653.py
create mode 100644 src/maistar_ranking/migrations/0006_auto_20171115_0653.py
create mode 100644 src/membership/migrations/0007_auto_20171115_0653.py
diff --git a/src/content/migrations/0006_auto_20171115_0653.py b/src/content/migrations/0006_auto_20171115_0653.py
new file mode 100644
index 0000000..648dc72
--- /dev/null
+++ b/src/content/migrations/0006_auto_20171115_0653.py
@@ -0,0 +1,91 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.7 on 2017-11-15 05:53
+from __future__ import unicode_literals
+
+import ckeditor_uploader.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('content', '0005_auto_20161012_2236'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='article',
+ name='content_de',
+ field=ckeditor_uploader.fields.RichTextUploadingField(verbose_name='Inhalt'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='content_en',
+ field=ckeditor_uploader.fields.RichTextUploadingField(blank=True, verbose_name='Content'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='headline_en',
+ field=models.CharField(blank=True, max_length=255, verbose_name='Headline'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='image',
+ field=models.ImageField(blank=True, null=True, upload_to='news/', verbose_name='Bild'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='slug',
+ field=models.SlugField(unique_for_month='date_created', verbose_name='Slug'),
+ ),
+ migrations.AlterField(
+ model_name='category',
+ name='image',
+ field=models.ImageField(blank=True, null=True, upload_to='news/categories/', verbose_name='Bild'),
+ ),
+ migrations.AlterField(
+ model_name='page',
+ name='content_de',
+ field=ckeditor_uploader.fields.RichTextUploadingField(blank=True, verbose_name='Inhalt'),
+ ),
+ migrations.AlterField(
+ model_name='page',
+ name='content_en',
+ field=ckeditor_uploader.fields.RichTextUploadingField(blank=True, verbose_name='Content'),
+ ),
+ migrations.AlterField(
+ model_name='page',
+ name='menu_name_de',
+ field=models.CharField(help_text='Ein kurzer Name für den Menüeintrag', max_length=255, verbose_name='Menü Name'),
+ ),
+ migrations.AlterField(
+ model_name='page',
+ name='menu_name_en',
+ field=models.CharField(blank=True, help_text='Ein kurzer Name für den Menüeintrag', max_length=255, verbose_name='Menu Name'),
+ ),
+ migrations.AlterField(
+ model_name='page',
+ name='pdf_de',
+ field=models.FileField(blank=True, null=True, upload_to='pdf/de/'),
+ ),
+ migrations.AlterField(
+ model_name='page',
+ name='pdf_en',
+ field=models.FileField(blank=True, null=True, upload_to='pdf/en/'),
+ ),
+ migrations.AlterField(
+ model_name='page',
+ name='template',
+ field=models.CharField(default='content/page.html', max_length=255, verbose_name='Vorlage'),
+ ),
+ migrations.AlterField(
+ model_name='page',
+ name='title_de',
+ field=models.CharField(help_text="The page title as you'd like it to be seen by the public", max_length=255, verbose_name='Titel'),
+ ),
+ migrations.AlterField(
+ model_name='page',
+ name='title_en',
+ field=models.CharField(blank=True, help_text="The page title as you'd like it to be seen by the public", max_length=255, verbose_name='Title'),
+ ),
+ ]
diff --git a/src/events/migrations/0008_auto_20171115_0653.py b/src/events/migrations/0008_auto_20171115_0653.py
new file mode 100644
index 0000000..61fc91a
--- /dev/null
+++ b/src/events/migrations/0008_auto_20171115_0653.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.7 on 2017-11-15 05:53
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('events', '0007_auto_20161012_2224'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='location',
+ name='country',
+ field=models.CharField(choices=[('GB', 'Vereinigtes Königreich'), ('AF', 'Afghanistan'), ('AX', 'Aland Islands'), ('AL', 'Albanien'), ('DZ', 'Algerien'), ('AS', 'Amerikanisch-Samoa'), ('AD', 'Andorra'), ('AO', 'Angola'), ('AI', 'Anguilla'), ('AQ', 'Antarktika'), ('AG', 'Antigua und Barbuda'), ('AR', 'Argentinien'), ('AM', 'Armenien'), ('AW', 'Aruba'), ('AU', 'Australien'), ('AT', 'Österreich'), ('AZ', 'Aserbaidschan'), ('BS', 'Bahamas'), ('BH', 'Bahrein'), ('BD', 'Bangladesch'), ('BB', 'Barbados'), ('BY', 'Weißrussland'), ('BE', 'Belgien'), ('BZ', 'Belize'), ('BJ', 'Benin'), ('BM', 'Bermuda'), ('BT', 'Bhutan'), ('BO', 'Bolivien'), ('BA', 'Bosnien und Herzegowina'), ('BW', 'Botswana'), ('BV', 'Bouvet Island'), ('BR', 'Brasilien'), ('IO', 'British Indian Ocean Territory'), ('BN', 'Brunei Darussalam'), ('BG', 'Bulgarien'), ('BF', 'Burkina Faso'), ('BI', 'Burundi'), ('KH', 'Kambodscha'), ('CM', 'Kamerun'), ('CA', 'Kanada'), ('CV', 'Cape Verde'), ('KY', 'Cayman Islands'), ('CF', 'Zentralafrikanische Republik'), ('TD', 'Tschad'), ('CL', 'Chile'), ('CN', 'China'), ('CX', 'Christmas Island'), ('CC', 'Cocos (Keeling) Islands'), ('CO', 'Kolumbien'), ('KM', 'Komoren'), ('CG', 'Kongo'), ('CD', 'Kongo, Demokratische Republik'), ('CK', 'Cook-Inseln'), ('CR', 'Costa Rica'), ('CI', "Cote d'Ivoire"), ('HR', 'Kroatien'), ('CU', 'Kuba'), ('CY', 'Zypern'), ('CZ', 'Tschechische Republik'), ('DK', 'Dänemark'), ('DJ', 'Dschibuti'), ('DM', 'Dominica'), ('DO', 'Dominikanische Republik'), ('EC', 'Ecuador'), ('EG', 'Ägypten'), ('SV', 'El Salvador'), ('GQ', 'Äquatorial-Guinea'), ('ER', 'Eritrea'), ('EE', 'Estland'), ('ET', 'Äthiopien'), ('FK', 'Falklandinseln (Malvinas)'), ('FO', 'Färöer-Inseln'), ('FJ', 'Fidschi'), ('FI', 'Finnland'), ('FR', 'Frankreich'), ('GF', 'Französisch-Guayana'), ('PF', 'Französisch-Polynesien'), ('TF', 'Französisch Südliche Territorien'), ('GA', 'Gabun'), ('GM', 'Gambia'), ('GE', 'Georgia'), ('DE', 'Deutschland'), ('GH', 'Ghana'), ('GI', 'Gibraltar'), ('GR', 'Griechenland'), ('GL', 'Grönland'), ('GD', 'Grenada'), ('GP', 'Guadeloupe'), ('GU', 'Guam'), ('GT', 'Guatemala'), ('GG', 'Guernsey'), ('GN', 'Guinea'), ('GW', 'Guinea-Bissau'), ('GY', 'Guyana'), ('HT', 'Haiti'), ('HM', 'Heard und McDonald Inseln'), ('VA', 'Heiliger Stuhl (Vatikanstadt)'), ('HN', 'Honduras'), ('HK', 'Hongkong'), ('HU', 'Ungarn'), ('IS', 'Island'), ('IN', 'Indien'), ('ID', 'Indonesien'), ('IR', 'Iran, Islamische Republik'), ('IQ', 'Irak'), ('IE', 'Irland'), ('IM', 'Isle of Man'), ('IL', 'Israel'), ('IT', 'Italien'), ('JM', 'Jamaika'), ('JP', 'Japan'), ('JE', 'Jersey'), ('JO', 'Jordan'), ('KZ', 'Kasachstan'), ('KE', 'Kenia'), ('KI', 'Kiribati'), ('KP', 'Korea, Demokratische Volksrepublik'), ('KR', 'Korea, Republik'), ('KW', 'Kuwait'), ('KG', 'Kirgisistan'), ('LA', 'Lao Demokratischen Volksrepublik'), ('LV', 'Lettland'), ('LB', 'Libanon'), ('LS', 'Lesotho'), ('LR', 'Liberia'), ('LY', 'Libyen'), ('LI', 'Liechtenstein'), ('LT', 'Litauen'), ('LU', 'Luxemburg'), ('MO', 'Macao'), ('MK', 'Mazedonien, die ehemalige jugoslawische Republik'), ('MG', 'Madagaskar'), ('MW', 'Malawi'), ('MY', 'Malaysia'), ('MV', 'Malediven'), ('ML', 'Mali'), ('MT', 'Malta'), ('MH', 'Marshall Islands'), ('MQ', 'Martinique'), ('MR', 'Mauretanien'), ('MU', 'Mauritius'), ('YT', 'Mayotte'), ('MX', 'Mexiko'), ('FM', 'Mikronesien, Föderierte Staaten von'), ('MD', 'Moldawien'), ('MC', 'Monaco'), ('MN', 'Mongolei'), ('ME', 'Montenegro'), ('MS', 'Montserrat'), ('MA', 'Marokko'), ('MZ', 'Mosambik'), ('MM', 'Myanmar'), ('NA', 'Namibia'), ('NR', 'Nauru'), ('NP', 'Nepal'), ('NL', 'Niederlande'), ('AN', 'Niederländische Antillen'), ('NC', 'Neukaledonien'), ('NZ', 'New Zealand'), ('NI', 'Nicaragua'), ('NE', 'Niger'), ('NG', 'Nigeria'), ('NU', 'Niue'), ('NF', 'Norfolk Island'), ('MP', 'Northern Mariana Islands'), ('NO', 'Norwegen'), ('OM', 'Oman'), ('PK', 'Pakistan'), ('PW', 'Palau'), ('PS', 'Palästinensische Autonomiegebiete'), ('PA', 'Panama'), ('PG', 'Papua-Neuguinea'), ('PY', 'Paraguay'), ('PE', 'Peru'), ('PH', 'Philippinen'), ('PN', 'Pitcairn'), ('PL', 'Polen'), ('PT', 'Portugal'), ('PR', 'Puerto Rico'), ('QA', 'Katar'), ('RE', 'Wiedervereinigung'), ('RO', 'Rumänien'), ('RU', 'Russischen Föderation'), ('RW', 'Ruanda'), ('BL', 'Saint Barthelemy'), ('SH', 'Saint Helena'), ('KN', 'Saint Kitts und Nevis'), ('LC', 'Santa Lucia'), ('MF', 'Santa Martin'), ('PM', 'Saint Pierre und Miquelon'), ('VC', 'Saint Vincent und die Grenadinen'), ('WS', 'Samoa'), ('SM', 'San Marino'), ('ST', 'Sao Tome und Principe'), ('SA', 'Saudi-Arabien'), ('SN', 'Senegal'), ('RS', 'Serbien'), ('SC', 'Seychellen'), ('SL', 'Sierra Leone'), ('SG', 'Singapur'), ('SK', 'Slowakei'), ('SI', 'Slowenien'), ('SB', 'Salomon-Inseln'), ('SO', 'Somalia'), ('ZA', 'Südafrika'), ('GS', 'Südgeorgien und die Südlichen Sandwichinseln'), ('ES', 'Spanien'), ('LK', 'Sri Lanka'), ('SD', 'Sudan'), ('SR', 'Suriname'), ('SJ', 'Svalbard und Jan Mayen'), ('SZ', 'Swaziland'), ('SE', 'Schweden'), ('CH', 'Schweiz'), ('SY', 'Arabische Republik Syrien'), ('TW', 'Taiwan, Province of China'), ('TJ', 'Tadschikistan'), ('TZ', 'Tansania, Vereinigte Republik'), ('TH', 'Thailand'), ('TL', 'Timor-Leste'), ('TG', 'Togo'), ('TK', 'Tokelau'), ('TO', 'Tonga'), ('TT', 'Trinidad und Tobago'), ('TN', 'Tunesien'), ('TR', 'Türkei'), ('TM', 'Turkmenistan'), ('TC', 'Turks-und Caicosinseln'), ('TV', 'Tuvalu'), ('UG', 'Uganda'), ('UA', 'Ukraine'), ('AE', 'Vereinigte Arabische Emirate'), ('US', 'Vereinigte Staaten'), ('UM', 'United States Minor Outlying Islands'), ('UY', 'Uruguay'), ('UZ', 'Usbekistan'), ('VU', 'Vanuatu'), ('VE', 'Venezuela'), ('VN', 'Vietnam'), ('VG', 'Virgin Islands, British'), ('VI', 'Virgin Islands, US'), ('WF', 'Wallis und Futuna'), ('EH', 'Westsahara'), ('YE', 'Jemen'), ('ZM', 'Sambia'), ('ZW', 'Zimbabwe')], max_length=2, verbose_name='Land'),
+ ),
+ ]
diff --git a/src/maistar_ranking/migrations/0006_auto_20171115_0653.py b/src/maistar_ranking/migrations/0006_auto_20171115_0653.py
new file mode 100644
index 0000000..a9c742f
--- /dev/null
+++ b/src/maistar_ranking/migrations/0006_auto_20171115_0653.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.7 on 2017-11-15 05:53
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('maistar_ranking', '0005_auto_20170218_1947'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='game',
+ options={'ordering': ('-event__start', '-id')},
+ ),
+ ]
diff --git a/src/membership/migrations/0007_auto_20171115_0653.py b/src/membership/migrations/0007_auto_20171115_0653.py
new file mode 100644
index 0000000..4d61c7d
--- /dev/null
+++ b/src/membership/migrations/0007_auto_20171115_0653.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.7 on 2017-11-15 05:53
+from __future__ import unicode_literals
+
+import django.contrib.auth.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('membership', '0006_auto_20160916_1759'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='membership',
+ name='gender',
+ field=models.CharField(blank=True, choices=[('m', 'Male'), ('f', 'Female')], max_length=1, null=True, verbose_name='Geschlecht'),
+ ),
+ migrations.AlterField(
+ model_name='membership',
+ name='username',
+ field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username'),
+ ),
+ ]
From cffbd30d7e3ef451039082944e0f8f88f36d5150 Mon Sep 17 00:00:00 2001
From: Xeniac
Date: Thu, 23 Nov 2017 22:01:38 +0100
Subject: [PATCH 09/19] Fixed: enumerate the Seasonrankings starting with 1
Fixed: Logging error when a value changed from/to None
---
src/mahjong_ranking/managers.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/mahjong_ranking/managers.py b/src/mahjong_ranking/managers.py
index 08560b4..674f734 100644
--- a/src/mahjong_ranking/managers.py
+++ b/src/mahjong_ranking/managers.py
@@ -184,7 +184,7 @@ class SeasonRankingManager(models.Manager):
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)):
+ for placement, ranking in enumerate(self.season_rankings(season), start=1):
ranking.placement = placement
ranking.save(force_update=True, update_fields=['placement'])
@@ -249,5 +249,5 @@ class KyuDanRankingManager(models.Manager):
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)}
+ 'old': old_value or 0, 'new': getattr(ranking, attr) or 0}
)
From 84880281c65d75785816c0ad29cc367a74e7e867 Mon Sep 17 00:00:00 2001
From: Xeniac
Date: Thu, 23 Nov 2017 22:02:40 +0100
Subject: [PATCH 10/19] Added a setting where the exported excel files should
be stored. Added a option to send the exported excel as mail attachment.
---
src/kasu/settings.py | 1 +
.../management/commands/export_ranking.py | 66 +++++++++++++++----
src/mahjong_ranking/models.py | 43 ++++++------
3 files changed, 79 insertions(+), 31 deletions(-)
diff --git a/src/kasu/settings.py b/src/kasu/settings.py
index a7a9009..ea63e90 100644
--- a/src/kasu/settings.py
+++ b/src/kasu/settings.py
@@ -273,6 +273,7 @@ KYU_RANKS = (
DAN_ALLOW_DROP_DOWN = True
MIN_HANCHANS_FOR_LADDER = 5
+RANKING_EXPORT_PATH = path.join(PROJECT_PATH, 'backup', 'mahjong_ranking')
try:
from .local_settings import * # Ignore PyLintBear (W0401, W0614)
diff --git a/src/mahjong_ranking/management/commands/export_ranking.py b/src/mahjong_ranking/management/commands/export_ranking.py
index 4092565..247f5a2 100644
--- a/src/mahjong_ranking/management/commands/export_ranking.py
+++ b/src/mahjong_ranking/management/commands/export_ranking.py
@@ -1,11 +1,15 @@
"""Export Mahjong Rankings as excel files."""
-import openpyxl
-from django.core.management.base import BaseCommand
-from django.utils.dateparse import parse_date
-from openpyxl.styles import Border
+import os
from datetime import date, time, datetime
+
+import openpyxl
+from django.conf import settings
+from django.core.management.base import BaseCommand
from django.utils import timezone
+from django.utils.dateparse import parse_date
+from django.core.mail import EmailMessage
+
from mahjong_ranking.models import SeasonRanking, KyuDanRanking
THIN_BORDER = openpyxl.styles.Side(style='thin', color="d3d7cf")
@@ -38,7 +42,19 @@ DATE_STYLE.font = DEFAULT_STYLE.font
DATE_STYLE.border = DEFAULT_STYLE.border
DATE_STYLE.number_format = 'dd.mm.yyyy'
+MAIL_BODY = """
+Hallo! Ich bin's dein Server.
+
+Ich habe gerade die Mahjong Rankings als Excel exportiert und dachte mir das
+ich sie dir am besten gleich schicke.
+Bitte versuche nicht auf diese E-Mail zu antworten.
+Ich bin nur ein dummes Programm.
+
+mit lieben Grüßen
+
+Der Kasu Server
+"""
def geneate_excel():
"""Generate an excel .xlsx spreadsheet from json data of the kyu/dan
rankings.
@@ -108,7 +124,7 @@ def export_season_rankings(workbook, until):
'style': 'content',
'width': 25},
{'col': 'C', 'title': '⌀ Platz', 'attr': 'avg_placement',
- 'style': 'int', 'width': 8},
+ 'style': 'float', 'width': 8},
{'col': 'D', 'title': '⌀ Punkte', 'attr': 'avg_score',
'style': 'float', 'width': 12},
{'col': 'E', 'title': 'Hanchans', 'attr': 'hanchan_count',
@@ -156,17 +172,43 @@ def export_kyu_dan_rankings(workbook, until):
class Command(BaseCommand):
"""Exports the SeasonRankings"""
+ filename = str()
+ until = datetime
def add_arguments(self, parser):
- parser.add_argument('--until', nargs='?', type=parse_date)
+ 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."""
- until = timezone.make_aware(
- datetime.combine(options['until'] or date.today(),
- time(23, 59, 59)))
+ self.until = timezone.make_aware(datetime.combine(
+ options['until'], time(23, 59, 59)
+ ))
+
+ self.filename = os.path.join(
+ settings.RANKING_EXPORT_PATH,
+ 'mahjong_rankings_{:%Y-%m-%d}.xlsx'.format(self.until)
+ )
workbook = geneate_excel()
- export_season_rankings(workbook, until=until)
- export_kyu_dan_rankings(workbook, until=until)
- workbook.save('mahjong_rankings_{:%Y-%m-%d}.xlsx'.format(until))
+ export_season_rankings(workbook, until=self.until)
+ export_kyu_dan_rankings(workbook, until=self.until)
+ os.makedirs(settings.RANKING_EXPORT_PATH, exist_ok=True)
+ workbook.save(self.filename)
+ if options['mail']:
+ self.send_mail(options['mail'])
+
+ def send_mail(self, recipients):
+ mail = EmailMessage(
+ subject='Mahjong Rankings vom {:%d.%m.%Y}'.format(self.until),
+ body=MAIL_BODY,
+ from_email=settings.DEFAULT_FROM_EMAIL,
+ to=recipients)
+ mail.attach_file(self.filename)
+ mail.send()
+
diff --git a/src/mahjong_ranking/models.py b/src/mahjong_ranking/models.py
index e31bff7..18878b9 100644
--- a/src/mahjong_ranking/models.py
+++ b/src/mahjong_ranking/models.py
@@ -4,7 +4,9 @@
# werden dürfen.
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
@@ -463,8 +465,12 @@ class KyuDanRanking(models.Model):
datetime.combine(self.legacy_date, time(0, 0, 0)))
else:
since = None
- else:
- since = self.last_hanchan_date or self.legacy_date
+ elif self.last_hanchan_date:
+ since = self.last_hanchan_date
+ elif self.legacy_date:
+ since = timezone.make_aware(
+ datetime.combine(self.legacy_date, time(0, 0, 0))
+ )
LOGGER.info(
"recalculating Kyu/Dan points for %(user)s since %(since)s...",
{'user': self.user, 'since': str(since)}
@@ -474,13 +480,11 @@ class KyuDanRanking(models.Model):
if until:
valid_hanchans = valid_hanchans.filter(start__lte=until)
- """ TODO: Hanchan Punkte nur neu berechnen wenn sie nach hachan_start
- lagen. Es müssen aber alle durch die Schleife rennen, damit die Punkte
- richtig gezählt werden."""
self.hanchan_count += valid_hanchans.count()
for hanchan in valid_hanchans:
hanchan.get_playerdata(self.user)
- if since and since < hanchan.start:
+ if since and hanchan.start < since:
+ print(hanchan, "<", since, "no recalc")
self.dan_points += hanchan.dan_points or 0
self.kyu_points += hanchan.kyu_points or 0
self.update_rank()
@@ -495,19 +499,19 @@ class KyuDanRanking(models.Model):
self.update_rank()
hanchan.update_playerdata(self.user)
hanchan.save(recalculate=False)
- self.won_hanchans += 1 if hanchan.placement == 1 else 0
- self.good_hanchans += 1 if hanchan.placement == 2 else 0
- self.last_hanchan_date = hanchan.start
- LOGGER.debug(
- 'id: %(id)d, start: %(start)s, placement: %(placement)d, '
- 'score: %(score)d, kyu points: %(kyu_points)d, dan points: '
- '%(dan_points)d, bonus points: %(bonus_points)d',
- {'id': hanchan.pk, 'start': hanchan.start,
- 'placement': hanchan.placement, 'score': hanchan.game_score,
- 'kyu_points': hanchan.kyu_points or 0,
- 'dan_points': hanchan.dan_points or 0,
- 'bonus_points': hanchan.bonus_points or 0}
- )
+ self.won_hanchans += 1 if hanchan.placement == 1 else 0
+ self.good_hanchans += 1 if hanchan.placement == 2 else 0
+ self.last_hanchan_date = hanchan.start
+ LOGGER.debug(
+ 'id: %(id)d, start: %(start)s, placement: %(placement)d, '
+ 'score: %(score)d, kyu points: %(kyu_points)d, dan points: '
+ '%(dan_points)d, bonus points: %(bonus_points)d',
+ {'id': hanchan.pk, 'start': hanchan.start,
+ 'placement': hanchan.placement, 'score': hanchan.game_score,
+ 'kyu_points': hanchan.kyu_points or 0,
+ 'dan_points': hanchan.dan_points or 0,
+ 'bonus_points': hanchan.bonus_points or 0}
+ )
self.save(force_update=True)
@@ -564,6 +568,7 @@ class KyuDanRanking(models.Model):
hanchan.kyu_points -= (self.kyu_points + hanchan.kyu_points)
self.kyu_points += hanchan.kyu_points
+
# TODO: Merkwürdige Methode die zwar funktioniert aber nicht sehr
# aussagekräfig ist. Überarbeiten?
def update_rank(self):
From c7b714c41be0a809e974d1a3fd115b8e2c61da16 Mon Sep 17 00:00:00 2001
From: Xeniac
Date: Thu, 7 Dec 2017 09:40:35 +0100
Subject: [PATCH 11/19] Added a setting where the exported excel files should
be stored. Added a option to send the exported excel as mail attachment.
---
TODO | 63 ++++++++++++++++-------------------
src/content/models.py | 11 +++---
src/events/models.py | 2 +-
src/events/views.py | 2 +-
src/mahjong_ranking/views.py | 2 +-
src/maistar_ranking/models.py | 2 +-
src/maistar_ranking/views.py | 2 +-
src/membership/models.py | 6 ++--
src/membership/views.py | 12 +++----
9 files changed, 48 insertions(+), 54 deletions(-)
diff --git a/TODO b/TODO
index b1e4522..0607631 100644
--- a/TODO
+++ b/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
diff --git a/src/content/models.py b/src/content/models.py
index df51317..869bb37 100644
--- a/src/content/models.py
+++ b/src/content/models.py
@@ -3,7 +3,7 @@ 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.urls import reverse
from django.db import models
from django.template.defaultfilters import slugify
from django.utils import timezone
@@ -262,7 +262,7 @@ class Page(models.Model):
def content(self):
"""Return the localized content, fallback to german if necessary."""
return mark_safe(
- getattr(self, "content_%s" % get_language(), self.content_de)
+ getattr(self, "content_%s" % get_language()) or self.content_de
)
@property
@@ -275,13 +275,12 @@ 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 getattr(self, "description_%s" % get_language()) or self.description_de
@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 getattr(self, "menu_name_%s" % get_language()) or self.menu_name_de
@property
def pdf_file(self):
@@ -291,7 +290,7 @@ class Page(models.Model):
@property
def title(self):
"""Return the localized title, fallback to german if necessary."""
- return getattr(self, "title_%s" % get_language(), self.title_de)
+ return getattr(self, "title_%s" % get_language()) or self.title_de
def clean(self):
"""set the URL path, the right content type, and scrub the HTML code."""
diff --git a/src/events/models.py b/src/events/models.py
index d805865..273c545 100644
--- a/src/events/models.py
+++ b/src/events/models.py
@@ -4,7 +4,7 @@ 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.urls import reverse
from django.db import models
from django.db.models import Q
from django.template.defaultfilters import slugify
diff --git a/src/events/views.py b/src/events/views.py
index e92bd3a..8ee2a35 100644
--- a/src/events/views.py
+++ b/src/events/views.py
@@ -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
diff --git a/src/mahjong_ranking/views.py b/src/mahjong_ranking/views.py
index 5394070..9855843 100644
--- a/src/mahjong_ranking/views.py
+++ b/src/mahjong_ranking/views.py
@@ -7,7 +7,7 @@ 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
diff --git a/src/maistar_ranking/models.py b/src/maistar_ranking/models.py
index 3c9706b..3099b83 100644
--- a/src/maistar_ranking/models.py
+++ b/src/maistar_ranking/models.py
@@ -2,7 +2,7 @@
import logging
-from django.core.urlresolvers import reverse
+from django.urls import reverse
from django.db import models
from django.db.models.signals import post_delete, post_save
from django.utils.translation import ugettext as _
diff --git a/src/maistar_ranking/views.py b/src/maistar_ranking/views.py
index 993aa75..115c1a9 100644
--- a/src/maistar_ranking/views.py
+++ b/src/maistar_ranking/views.py
@@ -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
diff --git a/src/membership/models.py b/src/membership/models.py
index 599b6b3..17e4312 100644
--- a/src/membership/models.py
+++ b/src/membership/models.py
@@ -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 _
@@ -226,8 +226,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:
diff --git a/src/membership/views.py b/src/membership/views.py
index fe9ed28..afdaad9 100644
--- a/src/membership/views.py
+++ b/src/membership/views.py
@@ -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)
From cf0e5e778cfb128701b240c817c0287d851697e1 Mon Sep 17 00:00:00 2001
From: Xeniac
Date: Thu, 7 Dec 2017 22:08:47 +0100
Subject: [PATCH 12/19] added on_delete in models an migrations for django 2.0
compatibility.
---
src/content/migrations/0001_initial.py | 8 +-
src/content/models.py | 13 +-
src/events/migrations/0001_initial.py | 328 +++++++++++++++++-
.../migrations/0004_auto_20150901_2204.py | 56 +--
src/events/models.py | 9 +-
.../migrations/0001_initial.py | 100 ++++--
src/mahjong_ranking/models.py | 117 +++----
.../migrations/0001_initial.py | 16 +-
src/maistar_ranking/models.py | 28 +-
src/membership/migrations/0001_initial.py | 100 ++++--
src/membership/models.py | 1 +
11 files changed, 590 insertions(+), 186 deletions(-)
diff --git a/src/content/migrations/0001_initial.py b/src/content/migrations/0001_initial.py
index 9714409..3bc2b66 100644
--- a/src/content/migrations/0001_initial.py
+++ b/src/content/migrations/0001_initial.py
@@ -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',
diff --git a/src/content/models.py b/src/content/models.py
index 869bb37..44030cc 100644
--- a/src/content/models.py
+++ b/src/content/models.py
@@ -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.urls 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 _
@@ -69,11 +69,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)
@@ -275,12 +278,14 @@ class Page(models.Model):
@property
def description(self):
"""Return the localized description, fallback to german if necessary."""
- return getattr(self, "description_%s" % get_language()) or self.description_de
+ return getattr(self,
+ "description_%s" % get_language()) or self.description_de
@property
def menu_name(self):
"""Return the localized menu name, fallback to german if necessary."""
- return getattr(self, "menu_name_%s" % get_language()) or self.menu_name_de
+ return getattr(self,
+ "menu_name_%s" % get_language()) or self.menu_name_de
@property
def pdf_file(self):
diff --git a/src/events/migrations/0001_initial.py b/src/events/migrations/0001_initial.py
index 9b6ff67..533f921 100644
--- a/src/events/migrations/0001_initial.py
+++ b/src/events/migrations/0001_initial.py
@@ -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),
),
]
diff --git a/src/events/migrations/0004_auto_20150901_2204.py b/src/events/migrations/0004_auto_20150901_2204.py
index c981e16..bf24d05 100644
--- a/src/events/migrations/0004_auto_20150901_2204.py
+++ b/src/events/migrations/0004_auto_20150901_2204.py
@@ -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),
),
]
diff --git a/src/events/models.py b/src/events/models.py
index 273c545..8e78596 100644
--- a/src/events/models.py
+++ b/src/events/models.py
@@ -4,10 +4,10 @@ import os
from ckeditor.fields import RichTextField
from django.conf import settings
from django.core.exceptions import ValidationError
-from django.urls 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,
diff --git a/src/mahjong_ranking/migrations/0001_initial.py b/src/mahjong_ranking/migrations/0001_initial.py
index e2bacf4..65b49cd 100644
--- a/src/mahjong_ranking/migrations/0001_initial.py
+++ b/src/mahjong_ranking/migrations/0001_initial.py
@@ -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'),
diff --git a/src/mahjong_ranking/models.py b/src/mahjong_ranking/models.py
index 18878b9..dcaafcd 100644
--- a/src/mahjong_ranking/models.py
+++ b/src/mahjong_ranking/models.py
@@ -9,8 +9,8 @@ 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 _
@@ -29,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)
@@ -86,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')
@@ -94,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'))
@@ -113,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'))
@@ -132,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'))
@@ -151,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'))
@@ -335,18 +335,21 @@ 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 = models.PositiveSmallIntegerField(blank=True, null=True)
legacy_dan_points = models.PositiveIntegerField(default=0)
legacy_kyu_points = models.PositiveIntegerField(default=0)
+ legacy_hanchan_count = models.PositiveIntegerField(default=0)
wins_in_a_row = models.PositiveIntegerField(default=0)
last_hanchan_date = models.DateTimeField(blank=True, null=True)
objects = managers.KyuDanRankingManager()
@@ -356,8 +359,8 @@ class KyuDanRanking(models.Model):
verbose_name = _(u'Kyū/Dan Ranking')
verbose_name_plural = _(u'Kyū/Dan Rankings')
- def __unicode__(self):
- if self.dan_points is not None:
+ 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)
@@ -395,7 +398,6 @@ class KyuDanRanking(models.Model):
self.dan_points += bonus_points
self.wins_in_a_row = 0
- # TODO: Komplett Überabreiten!
def append_tournament_bonuspoints(self, hanchan):
"""
Prüft ob es die letzte Hanchan in einem Turnier war. Wenn ja werden
@@ -408,29 +410,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:
@@ -451,7 +453,7 @@ class KyuDanRanking(models.Model):
force_recalc = True
if force_recalc:
# Setze alles auf die legacy Werte und berechne alles von neuem.
- self.dan = None
+ self.dan = self.legacy_dan
self.dan_points = self.legacy_dan_points or 0
self.kyu = None
self.kyu_points = self.legacy_kyu_points or 0
@@ -507,14 +509,14 @@ class KyuDanRanking(models.Model):
'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,
+ 'placement': hanchan.placement,
+ 'score': hanchan.game_score,
'kyu_points': hanchan.kyu_points or 0,
'dan_points': hanchan.dan_points or 0,
'bonus_points': hanchan.bonus_points or 0}
)
self.save(force_update=True)
-
def update_hanchan_points(self, hanchan):
"""
Berechne die Kyu bzw. Dan Punkte für eine Hanchan neu.
@@ -523,7 +525,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
@@ -547,6 +549,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:
@@ -568,38 +571,30 @@ class KyuDanRanking(models.Model):
hanchan.kyu_points -= (self.kyu_points + hanchan.kyu_points)
self.kyu_points += hanchan.kyu_points
-
- # TODO: Merkwürdige Methode die zwar funktioniert aber nicht sehr
- # aussagekräfig ist. Überarbeiten?
def update_rank(self):
- if self.dan and self.dan_points < 0:
- self.dan_points = 0
- self.dan = 1
+ print(self.user, self.dan, self.kyu)
+ old_dan = self.dan
+ if settings.DAN_ALLOW_DROP_DOWN and (self.dan or self.dan_points > 0):
+ self.dan = max((dan for min_points, dan in settings.DAN_RANKS if
+ self.dan_points > min_points))
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
+ self.dan = max((dan for min_points, dan in settings.DAN_RANKS if
+ self.max_dan_points > min_points))
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
else:
- for min_points, kyu_rank in settings.KYU_RANKS:
- if self.kyu_points > min_points:
- self.kyu = kyu_rank
- break
+ print(self, self.kyu_points)
+ self.kyu = max((kyu for min_points, kyu in settings.KYU_RANKS if
+ self.kyu_points > min_points))
+ self.wins_in_a_row = 0 if self.dan > old_dan else self.wins_in_a_row
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)
diff --git a/src/maistar_ranking/migrations/0001_initial.py b/src/maistar_ranking/migrations/0001_initial.py
index 79b5f59..352789f 100644
--- a/src/maistar_ranking/migrations/0001_initial.py
+++ b/src/maistar_ranking/migrations/0001_initial.py
@@ -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'),
diff --git a/src/maistar_ranking/models.py b/src/maistar_ranking/models.py
index 3099b83..3e65c3b 100644
--- a/src/maistar_ranking/models.py
+++ b/src/maistar_ranking/models.py
@@ -2,11 +2,11 @@
import logging
-from django.urls 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)
diff --git a/src/membership/migrations/0001_initial.py b/src/membership/migrations/0001_initial.py
index 816a3ff..7e3b7b5 100644
--- a/src/membership/migrations/0001_initial.py
+++ b/src/membership/migrations/0001_initial.py
@@ -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',
diff --git a/src/membership/models.py b/src/membership/models.py
index 17e4312..7157c5a 100644
--- a/src/membership/models.py
+++ b/src/membership/models.py
@@ -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)
From b20b988e5d1aef200f8422255184ff77d54bfd1d Mon Sep 17 00:00:00 2001
From: Xeniac
Date: Thu, 7 Dec 2017 22:54:18 +0100
Subject: [PATCH 13/19] Pinned Django on < 2.0 for better compatibility.
Mainlined traslation code for better DRY workflow. Fixed the EventDetail
Mixin.
---
requirements/base.txt | 4 ++--
src/content/models.py | 35 +++++++++++++++++------------------
src/events/mixins.py | 14 +++++++++-----
3 files changed, 28 insertions(+), 25 deletions(-)
diff --git a/requirements/base.txt b/requirements/base.txt
index 3537068..2c55f0f 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -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
\ No newline at end of file
+social-auth-core
diff --git a/src/content/models.py b/src/content/models.py
index 44030cc..3dec6e1 100644
--- a/src/content/models.py
+++ b/src/content/models.py
@@ -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."""
@@ -118,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):
@@ -149,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."""
@@ -264,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()) or self.content_de
- )
+ return mark_safe(get_localized(self, 'content'))
@property
def css_class(self):
@@ -278,24 +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()) or 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()) or 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()) or self.title_de
+ return get_localized(self, 'title')
def clean(self):
"""set the URL path, the right content type, and scrub the HTML code."""
diff --git a/src/events/mixins.py b/src/events/mixins.py
index 9b9f915..914cc55 100644
--- a/src/events/mixins.py
+++ b/src/events/mixins.py
@@ -48,9 +48,13 @@ class EventDetailMixin(object):
:return: a django QuerySets
"""
- try:
- self.event = models.Event.objects.get(pk=self.kwargs['event'])
- queryset = self.model.objects.filter(event=self.event)
- except models.Event.DoesNotExist:
- raise Http404(_('Event does not exist'))
+ 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()
From 1fdf88c6d2eff19599cfe5be7c8f831ec4f324ff Mon Sep 17 00:00:00 2001
From: Xeniac
Date: Fri, 22 Dec 2017 10:51:20 +0100
Subject: [PATCH 14/19] =?UTF-8?q?Diverse=20Umbauarbeiten=20f=C3=BCr=20das?=
=?UTF-8?q?=20neue=20Ranking.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../management/commands/resetdanranking.py | 24 +++--
.../management/commands/update_ranking.py | 4 +-
.../migrations/0006_auto_20171214_1318.py | 87 ++++++++++++++++
src/mahjong_ranking/models.py | 85 ++++++++--------
.../mahjong_ranking/player_dan_score.html | 6 +-
src/mahjong_ranking/views.py | 99 ++++++++++++++++++-
6 files changed, 243 insertions(+), 62 deletions(-)
create mode 100644 src/mahjong_ranking/migrations/0006_auto_20171214_1318.py
diff --git a/src/mahjong_ranking/management/commands/resetdanranking.py b/src/mahjong_ranking/management/commands/resetdanranking.py
index 2ead5e5..c01274e 100644
--- a/src/mahjong_ranking/management/commands/resetdanranking.py
+++ b/src/mahjong_ranking/management/commands/resetdanranking.py
@@ -17,16 +17,22 @@ class Command(BaseCommand):
parser.add_argument('reset_date', type=parse_date)
def handle(self, *args, **options):
- reset_date = timezone.make_aware(datetime.combine(options.get('reset_date'), time(23, 59, 59)))
- # models.KyuDanRanking.objects.update(until=reset_date, force_recalc=True)
- dan_rankigns = models.KyuDanRanking.objects.filter(dan__isnull=False)
- for ranking in dan_rankigns:
+ 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()
- ranking.legacy_hanchan_count = ranking.hanchan_count
- ranking.legacy_dan_points = ranking.dan_points
- ranking.legacy_kyu_points = ranking.kyu_points
+ 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()
-
-
diff --git a/src/mahjong_ranking/management/commands/update_ranking.py b/src/mahjong_ranking/management/commands/update_ranking.py
index d7a8fd7..4d6c1c5 100644
--- a/src/mahjong_ranking/management/commands/update_ranking.py
+++ b/src/mahjong_ranking/management/commands/update_ranking.py
@@ -23,11 +23,11 @@ class Command(BaseCommand):
def handle(self, *args, **options):
since = options.get('since', None)
until = options.get('until', None)
- force_recalc = options.get('forecerecalc', False)
+ force_recalc = options.get('forcerecalc')
if isinstance(since, date):
since = datetime.combine(since, time(0, 0, 0))
since = timezone.make_aware(since)
if isinstance(until, date):
until = datetime.combine(until, time(23, 59, 59))
until = timezone.make_aware(until)
- models.KyuDanRanking.objects.update(since=since, until=until, force_recalc=force_recalc)
\ No newline at end of file
+ models.KyuDanRanking.objects.update(since=since, until=until, force_recalc=force_recalc)
diff --git a/src/mahjong_ranking/migrations/0006_auto_20171214_1318.py b/src/mahjong_ranking/migrations/0006_auto_20171214_1318.py
new file mode 100644
index 0000000..0c39fec
--- /dev/null
+++ b/src/mahjong_ranking/migrations/0006_auto_20171214_1318.py
@@ -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),
+ ),
+ ]
diff --git a/src/mahjong_ranking/models.py b/src/mahjong_ranking/models.py
index dcaafcd..b5d12d6 100644
--- a/src/mahjong_ranking/models.py
+++ b/src/mahjong_ranking/models.py
@@ -347,9 +347,12 @@ class KyuDanRanking(models.Model):
hanchan_count = models.PositiveIntegerField(default=0)
legacy_date = models.DateField(blank=True, null=True)
legacy_dan = models.PositiveSmallIntegerField(blank=True, null=True)
- legacy_dan_points = models.PositiveIntegerField(default=0)
- legacy_kyu_points = models.PositiveIntegerField(default=0)
- legacy_hanchan_count = models.PositiveIntegerField(default=0)
+ 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()
@@ -371,12 +374,15 @@ 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
+ return
+ if self.wins_in_a_row >= 3 and self.dan < 9:
- if self.dan and 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
@@ -397,6 +403,7 @@ class KyuDanRanking(models.Model):
bonus_points, new_dan_rank)
self.dan_points += bonus_points
self.wins_in_a_row = 0
+ self.update_rank()
def append_tournament_bonuspoints(self, hanchan):
"""
@@ -455,18 +462,17 @@ class KyuDanRanking(models.Model):
# 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.kyu = None
+ 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 = 0
- self.won_hanchans = 0
- self.update_rank()
+ self.good_hanchans = self.legacy_good_hanchans or 0
+ self.won_hanchans = self.legacy_won_hanchans or 0
self.last_hanchan_date = None
- if self.legacy_date:
- since = timezone.make_aware(
- datetime.combine(self.legacy_date, time(0, 0, 0)))
- else:
- since = None
+ 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:
@@ -481,40 +487,27 @@ class KyuDanRanking(models.Model):
valid_hanchans = valid_hanchans.filter(start__gt=since)
if until:
valid_hanchans = valid_hanchans.filter(start__lte=until)
-
- self.hanchan_count += valid_hanchans.count()
for hanchan in valid_hanchans:
+ self.hanchan_count += 1
hanchan.get_playerdata(self.user)
if since and hanchan.start < since:
- print(hanchan, "<", since, "no recalc")
+ 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
- self.last_hanchan_date = hanchan.start
- LOGGER.debug(
- 'id: %(id)d, start: %(start)s, placement: %(placement)d, '
- 'score: %(score)d, kyu points: %(kyu_points)d, dan points: '
- '%(dan_points)d, bonus points: %(bonus_points)d',
- {'id': hanchan.pk, 'start': hanchan.start,
- 'placement': hanchan.placement,
- 'score': hanchan.game_score,
- 'kyu_points': hanchan.kyu_points or 0,
- 'dan_points': hanchan.dan_points or 0,
- 'bonus_points': hanchan.bonus_points or 0}
- )
+ self.won_hanchans += 1 if hanchan.placement == 1 else 0
+ self.good_hanchans += 1 if hanchan.placement == 2 else 0
+ self.last_hanchan_date = hanchan.start
self.save(force_update=True)
def update_hanchan_points(self, hanchan):
@@ -572,25 +565,27 @@ class KyuDanRanking(models.Model):
self.kyu_points += hanchan.kyu_points
def update_rank(self):
- print(self.user, self.dan, self.kyu)
- old_dan = self.dan
- if settings.DAN_ALLOW_DROP_DOWN and (self.dan or self.dan_points > 0):
- self.dan = max((dan for min_points, dan in settings.DAN_RANKS if
- self.dan_points > min_points))
- elif self.dan or self.dan_points > 0:
- self.dan = max((dan for min_points, dan in settings.DAN_RANKS if
- self.max_dan_points > min_points))
+ # 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.dan_points = 0
self.kyu = None
self.kyu_points = 0
self.wins_in_a_row = 0
+ # update Kyu ranking_
else:
- print(self, self.kyu_points)
- self.kyu = max((kyu for min_points, kyu in settings.KYU_RANKS if
- self.kyu_points > min_points))
- self.wins_in_a_row = 0 if self.dan > old_dan else self.wins_in_a_row
+ self.kyu = min((kyu for min_points, kyu in settings.KYU_RANKS
+ if self.kyu_points > min_points))
class SeasonRanking(models.Model):
diff --git a/src/mahjong_ranking/templates/mahjong_ranking/player_dan_score.html b/src/mahjong_ranking/templates/mahjong_ranking/player_dan_score.html
index 5a0aa33..2ef6db7 100755
--- a/src/mahjong_ranking/templates/mahjong_ranking/player_dan_score.html
+++ b/src/mahjong_ranking/templates/mahjong_ranking/player_dan_score.html
@@ -50,4 +50,8 @@
{% endfor %}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
+
+{% block buttonbar %}
+ Download
+{% endblock %}
diff --git a/src/mahjong_ranking/views.py b/src/mahjong_ranking/views.py
index 9855843..3580dbb 100644
--- a/src/mahjong_ranking/views.py
+++ b/src/mahjong_ranking/views.py
@@ -28,6 +28,56 @@ KYU_DAN_ORDER = { # map sort URL args to Django ORM order_by args
'-username': ('-user__username',)
}
+def getattr_recursive(obj, attr_string):
+ attr_list = attr_string.split('.')
+ return_value=None
+ for attr in attr_list:
+ print("Obj:", obj,'Attr:', attr)
+ return_value = getattr(obj, attr)
+ obj = return_value
+ return return_value
+
+def generate_sheet(workbook, title, columns_settings, object_list):
+ row = 1
+ ws = workbook.create_sheet()
+ ws.title = title
+ ws.syncHorizontal = True
+ ws.filterMode = True
+
+ # setup print orientation
+ ws.page_setup.orientation = ws.ORIENTATION_PORTRAIT
+ ws.page_setup.paperSize = ws.PAPERSIZE_A4
+ ws.page_setup.fitToWidth = True
+ ws.print_options.horizontalCentered = True
+
+ # setup page header
+ ws.oddHeader.left.text = title
+ ws.oddHeader.left.size = 14
+ ws.oddHeader.left.font = "Amerika Sans"
+ ws.oddHeader.left.color = "000000"
+
+ ws.oddHeader.right.text = str(date.today())
+ ws.oddHeader.right.size = 14
+ ws.oddHeader.right.font = "Amerika Sans"
+ ws.oddHeader.right.color = "000000"
+
+ # write table header
+ for column, data in enumerate(columns_settings, 1):
+ cell = ws.cell(column=column, row=row, value=data['title'])
+ cell.style = 'heading'
+
+ # write the table content
+ for line in object_list:
+ row += 1
+ for column, settings in enumerate(columns_settings, 1):
+ cell = ws.cell(column=column, row=row, value=getattr_recursive(line, settings['attr']))
+ cell.style = settings['style']
+
+ # set column widths
+ for settings in columns_settings:
+ ws.column_dimensions[settings['col']].width = settings['width']
+
+
class DeleteHanchan(EventDetailMixin, PermissionRequiredMixin,
generic.DeleteView):
@@ -117,9 +167,6 @@ class EventRankingList(EventDetailMixin, generic.ListView):
model = models.EventRanking
-
-
-
class KyuDanRankingList(MahjongMixin, generic.ListView):
"""List all Players with an Kyu or Dan score. """
default_order = '-score'
@@ -133,7 +180,6 @@ class KyuDanRankingList(MahjongMixin, generic.ListView):
]
return super(KyuDanRankingList, self).dispatch(request, *args, **kwargs)
-
def get_queryset(self):
queryset = models.KyuDanRanking.objects.filter(
hanchan_count__gt=0).order_by(*self.order_by)
@@ -160,7 +206,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'))
@@ -168,6 +213,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):
@@ -186,12 +234,47 @@ class PlayerScore(LoginRequiredMixin, generic.ListView):
context['ladder_ranking'] = models.SeasonRanking(user=self.user)
return context
+ def get_xlsx(self, request, *args, **kwargs):
+ from management.commands.export_ranking import geneate_excel
+ self.object_list = self.get_queryset()
+ allow_empty = self.get_allow_empty()
+ response = django.http.HttpResponse(
+ content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
+ response[
+ 'Content-Disposition'] = 'attachment; filename="{xlsx_filename}"'.format(xlsx_filename=self.xlsx_filename)
+ xlxs_workbook = geneate_excel()
+ print(self.xlsx_columns)
+ generate_sheet(xlxs_workbook,
+ title=self.xlsx_filename,
+ columns_settings=self.xlsx_columns,
+ object_list=self.object_list
+ )
+ xlxs_workbook.create_sheet()
+ xlxs_workbook.save(response)
+ return response
+
class PlayerDanScore(PlayerScore):
template_name = 'mahjong_ranking/player_dan_score.html'
+ xlsx_columns = (
+ {'col': 'A', 'title': 'Beginn', 'attr': 'start', 'style': 'date', 'width': 14},
+ {'col': 'B', 'title': 'Platzierung', 'attr': 'placement', 'style': 'int', 'width': 8},
+ {'col': 'C', 'title': 'Spieler 1', 'attr': 'player1.username', 'style': 'content', 'width': 8},
+ {'col': 'D', 'title': 'Punkte', 'attr': 'player1_game_score', 'style': 'int', 'width': 8},
+ {'col': 'E', 'title': 'Spieler 2', 'attr': 'player2.username', 'style': 'content', 'width': 8},
+ {'col': 'F', 'title': 'Punkte', 'attr': 'player2_game_score', 'style': 'int', 'width': 8},
+ {'col': 'G', 'title': 'Spieler 3', 'attr': 'player3.username', 'style': 'content', 'width': 8},
+ {'col': 'H', 'title': 'Punkte', 'attr': 'player3_game_score', 'style': 'int', 'width': 8},
+ {'col': 'I', 'title': 'Spieler 4', 'attr': 'player4.username', 'style': 'content', 'width': 8},
+ {'col': 'J', 'title': 'Punkte', 'attr': 'player4_game_score', 'style': 'int', 'width': 8},
+ {'col': 'K', 'title': 'Dan Punkte', 'attr': 'dan_points', 'style': 'int', 'width': 8},
+ {'col': 'L', 'title': 'Anmerkung', 'attr': 'comment', 'style': 'content', 'width': 8},
+ )
def get_queryset(self):
kyu_dan_ranking = models.KyuDanRanking.objects.get(user=self.user)
+ self.xlsx_filename = "{username}_dan_score.xlsx".format(
+ username=self.user.username)
return models.Hanchan.objects.dan_hanchans(user=self.user,
since=kyu_dan_ranking.legacy_date)
@@ -200,6 +283,8 @@ class PlayerInvalidScore(PlayerScore):
template_name = 'mahjong_ranking/player_invalid_score.html'
def get_queryset(self):
+ self.xlsx_filename = "{username}_invalid_score.xlsx".format(
+ username=self.user.username)
return models.Hanchan.objects.unconfirmed(user=self.user)
@@ -207,6 +292,8 @@ class PlayerKyuScore(PlayerScore):
template_name = 'mahjong_ranking/player_kyu_score.html'
def get_queryset(self):
+ self.xlsx_filename = "{username}_kyu_score.xlsx".format(
+ username=self.user.username)
return models.Hanchan.objects.kyu_hanchans(self.user)
@@ -224,6 +311,8 @@ class PlayerLadderScore(PlayerScore):
def get_queryset(self, **kwargs):
self.season = int(self.request.GET.get('season', date.today().year))
+ self.xlsx_filename = "{username}_ladder ({season}).xlsx".format(
+ username=self.user.username, season=self.season)
hanchan_list = models.Hanchan.objects.season_hanchans(
user=self.user,
season=self.season
From 8ab99ec0394d5894d92458a50b30287c211ca5be Mon Sep 17 00:00:00 2001
From: Xeniac
Date: Fri, 22 Dec 2017 10:54:11 +0100
Subject: [PATCH 15/19] =?UTF-8?q?Noch=20mehr=20Einstellungen=20f=C3=BCr=20?=
=?UTF-8?q?Kasu=20Ranking,=20um=20es=20an=20das=20neue=20Dan=20System=20an?=
=?UTF-8?q?passen=20zu=20k=C3=B6nnen.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/kasu/settings.py | 24 +++++++++++++++++-------
1 file changed, 17 insertions(+), 7 deletions(-)
diff --git a/src/kasu/settings.py b/src/kasu/settings.py
index ea63e90..111e0f9 100644
--- a/src/kasu/settings.py
+++ b/src/kasu/settings.py
@@ -229,7 +229,7 @@ LOGGING = {
'loggers': {
'django': {
'handlers': ['console'],
- 'level': 'INFO',
+ 'level': 'DEBUG',
'propagate': True,
},
'django.request': {
@@ -246,6 +246,20 @@ 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_ALLOW_DROP_DOWN = True
DAN_RANKS = (
(80, 9),
(70, 8),
@@ -255,7 +269,7 @@ DAN_RANKS = (
(30, 4),
(20, 3),
(10, 2),
- (0, 1),
+ (-1, 1),
)
KYU_RANKS = (
@@ -268,13 +282,9 @@ KYU_RANKS = (
(15, 7),
(10, 8),
(5, 9),
- (0, 10),
+ (-1, 10),
)
-DAN_ALLOW_DROP_DOWN = True
-MIN_HANCHANS_FOR_LADDER = 5
-RANKING_EXPORT_PATH = path.join(PROJECT_PATH, 'backup', 'mahjong_ranking')
-
try:
from .local_settings import * # Ignore PyLintBear (W0401, W0614)
except ImportError:
From 10c27784eedb66a09444b196e70f52c462765a28 Mon Sep 17 00:00:00 2001
From: Xeniac
Date: Tue, 26 Dec 2017 21:45:39 +0100
Subject: [PATCH 16/19] =?UTF-8?q?XLSX=20Export=20vereinheitlicht.=20Spiele?=
=?UTF-8?q?r=20Hanchanlisten=20k=C3=B6nnen=20nun=20als=20XLSX=20exportiert?=
=?UTF-8?q?=20werden.=20Anpassungen=20in=20den=20Einstellungen=20f=C3=BCr?=
=?UTF-8?q?=20die=20parametisierten=20Kyu/Dan=20Berechnung.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 1 +
.../migrations/0007_auto_20171214_1215.py | 27 +++
.../migrations/0009_auto_20171214_1215.py | 32 +++
src/kasu/settings.py | 2 +
.../templates/django/forms/widgets/date.html | 4 +
.../django/forms/widgets/datetime.html | 4 +
.../django/forms/widgets/html5input.html | 1 +
.../templates/django/forms/widgets/time.html | 4 +
src/kasu/urls.py | 2 +-
src/kasu/xlsx.py | 147 ++++++++++++
.../management/commands/export_ranking.py | 214 ------------------
.../management/commands/exportranking.py | 126 +++++++++++
.../{update_ranking.py => updateranking.py} | 27 ++-
src/mahjong_ranking/models.py | 14 +-
.../mahjong_ranking/player_dan_score.html | 4 +
.../mahjong_ranking/player_invalid_score.html | 10 +-
.../mahjong_ranking/player_kyu_score.html | 10 +-
.../mahjong_ranking/player_ladder_score.html | 5 +-
src/mahjong_ranking/views.py | 214 +++++++++++-------
.../migrations/0007_auto_20171214_1215.py | 52 +++++
src/membership/models.py | 4 +
.../membership/membership_detail.html | 5 +-
22 files changed, 589 insertions(+), 320 deletions(-)
create mode 100644 src/content/migrations/0007_auto_20171214_1215.py
create mode 100644 src/events/migrations/0009_auto_20171214_1215.py
create mode 100644 src/kasu/templates/django/forms/widgets/date.html
create mode 100644 src/kasu/templates/django/forms/widgets/datetime.html
create mode 100644 src/kasu/templates/django/forms/widgets/html5input.html
create mode 100644 src/kasu/templates/django/forms/widgets/time.html
create mode 100644 src/kasu/xlsx.py
delete mode 100644 src/mahjong_ranking/management/commands/export_ranking.py
create mode 100644 src/mahjong_ranking/management/commands/exportranking.py
rename src/mahjong_ranking/management/commands/{update_ranking.py => updateranking.py} (54%)
create mode 100644 src/maistar_ranking/migrations/0007_auto_20171214_1215.py
diff --git a/.gitignore b/.gitignore
index 7041e31..d8443f0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -62,6 +62,7 @@ docs/_build/
target/
#Django Development
+backup/
/bower_components/
/media/
/node_modules/
diff --git a/src/content/migrations/0007_auto_20171214_1215.py b/src/content/migrations/0007_auto_20171214_1215.py
new file mode 100644
index 0000000..61398c5
--- /dev/null
+++ b/src/content/migrations/0007_auto_20171214_1215.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.8 on 2017-12-14 11:15
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('content', '0006_auto_20171115_0653'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='article',
+ name='author',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='Autor'),
+ ),
+ migrations.AlterField(
+ model_name='article',
+ name='category',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='content.Category', verbose_name='Kategorie'),
+ ),
+ ]
diff --git a/src/events/migrations/0009_auto_20171214_1215.py b/src/events/migrations/0009_auto_20171214_1215.py
new file mode 100644
index 0000000..22dc786
--- /dev/null
+++ b/src/events/migrations/0009_auto_20171214_1215.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.8 on 2017-12-14 11:15
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('events', '0008_auto_20171115_0653'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='event',
+ name='location',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='events.Location'),
+ ),
+ migrations.AlterField(
+ model_name='photo',
+ name='event',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='events.Event'),
+ ),
+ migrations.AlterField(
+ model_name='photo',
+ name='photographer',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
+ ),
+ ]
diff --git a/src/kasu/settings.py b/src/kasu/settings.py
index 111e0f9..e38a68c 100644
--- a/src/kasu/settings.py
+++ b/src/kasu/settings.py
@@ -259,7 +259,9 @@ TOURNAMENT_WIN_BONUSPOINTS = 4
TOURNAMENT_FLAWLESS_VICTORY_BONUSPOINTS = 8
# Old Dan System
+DAN_3_WINS_IN_A_ROW = True
DAN_ALLOW_DROP_DOWN = True
+
DAN_RANKS = (
(80, 9),
(70, 8),
diff --git a/src/kasu/templates/django/forms/widgets/date.html b/src/kasu/templates/django/forms/widgets/date.html
new file mode 100644
index 0000000..08c783d
--- /dev/null
+++ b/src/kasu/templates/django/forms/widgets/date.html
@@ -0,0 +1,4 @@
+{% with type="date" %}
+{% include "django/forms/widgets/html5input.html" %}
+{% endwith %}
+
diff --git a/src/kasu/templates/django/forms/widgets/datetime.html b/src/kasu/templates/django/forms/widgets/datetime.html
new file mode 100644
index 0000000..e3f044b
--- /dev/null
+++ b/src/kasu/templates/django/forms/widgets/datetime.html
@@ -0,0 +1,4 @@
+{% with type="datetime-local" %}
+{% include "django/forms/widgets/html5input.html" %}
+{% endwith %}
+
diff --git a/src/kasu/templates/django/forms/widgets/html5input.html b/src/kasu/templates/django/forms/widgets/html5input.html
new file mode 100644
index 0000000..f23b0ad
--- /dev/null
+++ b/src/kasu/templates/django/forms/widgets/html5input.html
@@ -0,0 +1 @@
+
diff --git a/src/kasu/templates/django/forms/widgets/time.html b/src/kasu/templates/django/forms/widgets/time.html
new file mode 100644
index 0000000..c85e335
--- /dev/null
+++ b/src/kasu/templates/django/forms/widgets/time.html
@@ -0,0 +1,4 @@
+{% with type="time" %}
+{% include "django/forms/widgets/html5input.html" %}
+{% endwith %}
+
diff --git a/src/kasu/urls.py b/src/kasu/urls.py
index 1af6c8a..05be4a4 100644
--- a/src/kasu/urls.py
+++ b/src/kasu/urls.py
@@ -32,7 +32,7 @@ urlpatterns = [ # Ignore PyLintBear (C0103)
url(r'^add_page/(?P[\+\.\-\d\w\/]+)/$',
views.PageAddForm.as_view(), name='add-page'),
url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
- url(r'^admin/', include(admin.site.urls)),
+ url(r'^admin/', admin.site.urls),
url(r'^ckeditor/', include('ckeditor_uploader.urls')),
url(r'^comments/', include('django_comments.urls')),
url(r'^edit_page/(?P[\+\.\-\d\w\/]+)/$',
diff --git a/src/kasu/xlsx.py b/src/kasu/xlsx.py
new file mode 100644
index 0000000..4a26a88
--- /dev/null
+++ b/src/kasu/xlsx.py
@@ -0,0 +1,147 @@
+"""
+Helper to generate XLSX Spreadsheets in an uniform way.
+"""
+from datetime import date
+
+import openpyxl
+from openpyxl.styles import NamedStyle, Font, Border, Side
+
+DEFAULT_FONT = Font(name='Philosopher', size=10, bold=False, color='000000')
+THIN_BORDER = Border(
+ bottom=Side(style='thin', color="d3d7cf"),
+ top=Side(style='thin', color="d3d7cf"))
+XLSX_STYLES = dict()
+
+XLSX_STYLES['Content'] = NamedStyle(
+ name='Content',
+ font=DEFAULT_FONT,
+ border=THIN_BORDER
+)
+
+XLSX_STYLES['Headline'] = NamedStyle(
+ name="Headline",
+ font=openpyxl.styles.Font(name='Philosopher', size=11,
+ bold=True, color='ffffff'),
+ fill=openpyxl.styles.PatternFill(fill_type='solid',
+ start_color='a40000',
+ end_color='a40000')
+)
+
+XLSX_STYLES['Date'] = NamedStyle(
+ name='Date',
+ font=DEFAULT_FONT,
+ border=THIN_BORDER,
+ number_format='dd.mm.yyyy'
+)
+
+XLSX_STYLES['Date Time'] = NamedStyle(
+ name='Date Time',
+ font=DEFAULT_FONT,
+ border=THIN_BORDER,
+ number_format='dd.mm.yyyy hh:MM'
+)
+
+XLSX_STYLES['Float'] = NamedStyle(
+ name='Float',
+ font=DEFAULT_FONT,
+ border=THIN_BORDER,
+ number_format='#,##0.00'
+)
+
+XLSX_STYLES['Integer'] = NamedStyle(
+ name='Integer',
+ font=DEFAULT_FONT,
+ border=THIN_BORDER,
+ number_format='#,##0'
+)
+
+
+def getattr_recursive(obj, attr_string):
+ """A recusive version of gettattr. the attr_string is splitted on the ".".
+
+ :param obj: a python object.
+ :param attr_string: the desired attribute of the object.
+ :return: a getattr_recursice(obj, 'attr1.attr2') will return the value of attr2 of attr1 from obj
+ """
+ attr_list = attr_string.split('.')
+ return_value = None
+ for attr in attr_list:
+ return_value = getattr(obj, attr)
+ obj = return_value
+ return return_value
+
+
+class Workbook(object):
+ workbook = None
+
+ def __init__(self):
+ """Generate an XLSX Workbook in memory
+
+ :rtype: object
+ """
+ self.workbook = openpyxl.Workbook()
+ [self.workbook.add_named_style(style) for style in XLSX_STYLES.values()]
+ [self.workbook.remove(sheet) for sheet in self.workbook.worksheets]
+
+ def generate_sheet(self, title, columns_settings, object_list,
+ orientation='landscape'):
+ """
+
+ :param title: Title of the Sheet
+ :param columns_settings: a list of dicts for the settings of each column
+ :param object_list: List of objects that should be added to the sheet
+ :param orientation: Paper Orientation 'landscape' or 'portrait'
+ """
+ row = 1
+ ws = self.workbook.create_sheet()
+ ws.title = title
+ ws.syncHorizontal = True
+ ws.filterMode = True
+
+ # setup print orientation
+ ws.page_setup.fitToHeight = 0
+ ws.page_setup.fitToWidth = 1
+ if orientation == 'landscape':
+ ws.page_setup.orientation = ws.ORIENTATION_LANDSCAPE
+ else:
+ ws.page_setup.orientation = ws.ORIENTATION_PORTRAIT
+ ws.page_setup.paperSize = ws.PAPERSIZE_A4
+ ws.print_options.horizontalCentered = True
+
+ # setup page header
+ ws.oddHeader.left.text = title
+ ws.oddHeader.left.size = 14
+ ws.oddHeader.left.font = "Amerika Sans"
+ ws.oddHeader.left.color = "000000"
+
+ ws.oddHeader.right.text = str(date.today())
+ ws.oddHeader.right.size = 14
+ ws.oddHeader.right.font = "Amerika Sans"
+ ws.oddHeader.right.color = "000000"
+
+ # write table header
+ for column, data in enumerate(columns_settings, 1):
+ cell = ws.cell(column=column, row=row, value=data['title'])
+ cell.style = 'Headline'
+ row += 1
+
+ # write the table content
+ for line in object_list:
+ for column, settings in enumerate(columns_settings, 1):
+ cell = ws.cell(column=column, row=row,
+ value=getattr_recursive(line, settings['attr']))
+ cell.style = settings['style']
+ row += 1
+
+ # write table footer
+ for column, settings in enumerate(columns_settings, 1):
+ cell = ws.cell(column=column, row=row, value=settings.get('footer'))
+ cell.style = settings['style']
+ row += 1
+
+ # set column widths
+ for settings in columns_settings:
+ ws.column_dimensions[settings['col']].width = settings['width']
+
+ def save(self, *args, **kwargs):
+ return self.workbook.save(*args, **kwargs)
diff --git a/src/mahjong_ranking/management/commands/export_ranking.py b/src/mahjong_ranking/management/commands/export_ranking.py
deleted file mode 100644
index 247f5a2..0000000
--- a/src/mahjong_ranking/management/commands/export_ranking.py
+++ /dev/null
@@ -1,214 +0,0 @@
-"""Export Mahjong Rankings as excel files."""
-
-import os
-from datetime import date, time, datetime
-
-import openpyxl
-from django.conf import settings
-from django.core.management.base import BaseCommand
-from django.utils import timezone
-from django.utils.dateparse import parse_date
-from django.core.mail import EmailMessage
-
-from mahjong_ranking.models import SeasonRanking, KyuDanRanking
-
-THIN_BORDER = openpyxl.styles.Side(style='thin', color="d3d7cf")
-
-HEADING_STYLE = openpyxl.styles.NamedStyle(name="heading")
-HEADING_STYLE.font = openpyxl.styles.Font(name='Philosopher', size=11,
- bold=True, color='ffffff')
-HEADING_STYLE.fill = openpyxl.styles.PatternFill(fill_type='solid',
- start_color='a40000',
- end_color='a40000')
-
-DEFAULT_STYLE = openpyxl.styles.NamedStyle(name='content')
-DEFAULT_STYLE.font = openpyxl.styles.Font(name='Philosopher', size=10,
- bold=False, color='000000')
-DEFAULT_STYLE.border = openpyxl.styles.Border(bottom=THIN_BORDER,
- top=THIN_BORDER)
-
-INT_STYLE = openpyxl.styles.NamedStyle(name='int')
-INT_STYLE.font = DEFAULT_STYLE.font
-INT_STYLE.border = DEFAULT_STYLE.border
-INT_STYLE.number_format = '#,##0'
-
-FLOAT_STYLE = openpyxl.styles.NamedStyle(name='float')
-FLOAT_STYLE.font = DEFAULT_STYLE.font
-FLOAT_STYLE.border = DEFAULT_STYLE.border
-FLOAT_STYLE.number_format = '#,##0.00'
-
-DATE_STYLE = openpyxl.styles.NamedStyle(name='date')
-DATE_STYLE.font = DEFAULT_STYLE.font
-DATE_STYLE.border = DEFAULT_STYLE.border
-DATE_STYLE.number_format = 'dd.mm.yyyy'
-
-MAIL_BODY = """
-Hallo! Ich bin's dein Server.
-
-Ich habe gerade die Mahjong Rankings als Excel exportiert und dachte mir das
-ich sie dir am besten gleich schicke.
-
-Bitte versuche nicht auf diese E-Mail zu antworten.
-Ich bin nur ein dummes Programm.
-
-mit lieben Grüßen
-
-Der Kasu Server
-"""
-def geneate_excel():
- """Generate an excel .xlsx spreadsheet from json data of the kyu/dan
- rankings.
-
- :param json_data: The ladder ranking as JSON export."""
- workbook = openpyxl.Workbook()
- workbook.add_named_style(HEADING_STYLE)
- workbook.add_named_style(DEFAULT_STYLE)
- workbook.add_named_style(INT_STYLE)
- workbook.add_named_style(FLOAT_STYLE)
- workbook.add_named_style(DATE_STYLE)
-
- for sheet in workbook.worksheets:
- workbook.remove(sheet)
- return workbook
-
-
-def generate_sheet(workbook, title, columns_settings, json_data):
- row = 1
- ws = workbook.create_sheet()
- ws.title = title
- ws.syncHorizontal = True
- ws.filterMode = True
-
- # setup print orientation
- ws.page_setup.orientation = ws.ORIENTATION_PORTRAIT
- ws.page_setup.paperSize = ws.PAPERSIZE_A4
- ws.page_setup.fitToWidth = True
- ws.print_options.horizontalCentered = True
-
- # setup page header
- ws.oddHeader.left.text = title
- ws.oddHeader.left.size = 14
- ws.oddHeader.left.font = "Amerika Sans"
- ws.oddHeader.left.color = "000000"
-
- ws.oddHeader.right.text = str(date.today())
- ws.oddHeader.right.size = 14
- ws.oddHeader.right.font = "Amerika Sans"
- ws.oddHeader.right.color = "000000"
-
- # write table header
- for column, data in enumerate(columns_settings, 1):
- cell = ws.cell(column=column, row=row, value=data['title'])
- cell.style = 'heading'
-
- # write the table content
- for line in json_data:
- row += 1
- for column, settings in enumerate(columns_settings, 1):
- cell = ws.cell(column=column, row=row, value=line[settings['attr']])
- cell.style = settings['style']
-
- # set column widths
- for settings in columns_settings:
- ws.column_dimensions[settings['col']].width = settings['width']
-
-
-def export_season_rankings(workbook, until):
- SeasonRanking.objects.update(until=until)
- json_data = SeasonRanking.objects.json_data()
- title = "Mahjong Ladder - {}".format(until.year)
- columns_settings = (
- {'col': 'A', 'title': 'Rang', 'attr': 'placement', 'style': 'int',
- 'width': 8},
- {'col': 'B', 'title': 'Spitzname', 'attr': 'username',
- 'style': 'content',
- 'width': 25},
- {'col': 'C', 'title': '⌀ Platz', 'attr': 'avg_placement',
- 'style': 'float', 'width': 8},
- {'col': 'D', 'title': '⌀ Punkte', 'attr': 'avg_score',
- 'style': 'float', 'width': 12},
- {'col': 'E', 'title': 'Hanchans', 'attr': 'hanchan_count',
- 'style': 'int', 'width': 10},
- {'col': 'F', 'title': 'Gut', 'attr': 'good_hanchans',
- 'style': 'int', 'width': 5},
- {'col': 'G', 'title': 'Gewonnen', 'attr': 'won_hanchans',
- 'style': 'int', 'width': 10},
- )
- generate_sheet(
- workbook=workbook,
- title=title,
- columns_settings=columns_settings,
- json_data=json_data)
-
-
-def export_kyu_dan_rankings(workbook, until):
- KyuDanRanking.objects.update(until=until)
- json_data = KyuDanRanking.objects.json_data()
- title = "Kyū & Dan Rankings"
- columns_settings = (
- {'col': 'A', 'title': 'Spitzname', 'attr': 'username',
- 'style': 'content', 'width': 14},
- {'col': 'B', 'title': 'Voller Name', 'attr': 'full_name',
- 'style': 'content', 'width': 20},
- {'col': 'C', 'title': 'Rang', 'attr': 'rank',
- 'style': 'content', 'width': 8},
- {'col': 'D', 'title': 'Punkte', 'attr': 'points',
- 'style': 'int', 'width': 8},
- {'col': 'E', 'title': 'Hanchans', 'attr': 'hanchan_count',
- 'style': 'int', 'width': 10},
- {'col': 'F', 'title': 'Gut', 'attr': 'good_hanchans',
- 'style': 'int', 'width': 5},
- {'col': 'G', 'title': 'Gewonnen', 'attr': 'won_hanchans',
- 'style': 'int', 'width': 8},
- {'col': 'H', 'title': 'letzte Hanchan', 'attr': 'last_hanchan_date',
- 'style': 'date', 'width': 16},
- )
- generate_sheet(
- workbook=workbook,
- title=title,
- columns_settings=columns_settings,
- json_data=json_data)
-
-
-class Command(BaseCommand):
- """Exports the SeasonRankings"""
- filename = str()
- until = datetime
-
- def add_arguments(self, parser):
- parser.add_argument(
- '--until', nargs='?', type=parse_date,
- default=date.today(), metavar='YYYY-MM-DD',
- help='Calculate and export rankings until the given date.')
- parser.add_argument(
- '--mail', nargs='*', type=str, metavar='user@example.com',
- help='Send the spreadsheet via eMail to the given recipient.')
-
- def handle(self, *args, **options):
- """Exports the current ladder ranking in a spreadsheet.
- This is useful as a backup in form of a hardcopy."""
- self.until = timezone.make_aware(datetime.combine(
- options['until'], time(23, 59, 59)
- ))
-
- self.filename = os.path.join(
- settings.RANKING_EXPORT_PATH,
- 'mahjong_rankings_{:%Y-%m-%d}.xlsx'.format(self.until)
- )
- workbook = geneate_excel()
- export_season_rankings(workbook, until=self.until)
- export_kyu_dan_rankings(workbook, until=self.until)
- os.makedirs(settings.RANKING_EXPORT_PATH, exist_ok=True)
- workbook.save(self.filename)
- if options['mail']:
- self.send_mail(options['mail'])
-
- def send_mail(self, recipients):
- mail = EmailMessage(
- subject='Mahjong Rankings vom {:%d.%m.%Y}'.format(self.until),
- body=MAIL_BODY,
- from_email=settings.DEFAULT_FROM_EMAIL,
- to=recipients)
- mail.attach_file(self.filename)
- mail.send()
-
diff --git a/src/mahjong_ranking/management/commands/exportranking.py b/src/mahjong_ranking/management/commands/exportranking.py
new file mode 100644
index 0000000..d7440a3
--- /dev/null
+++ b/src/mahjong_ranking/management/commands/exportranking.py
@@ -0,0 +1,126 @@
+"""Export Mahjong Rankings as excel files."""
+
+import os
+from datetime import date, time, datetime
+
+from django.conf import settings
+from django.core.mail import EmailMessage
+from django.core.management.base import BaseCommand
+from django.utils import timezone
+from django.utils.dateparse import parse_date
+
+from kasu import xlsx
+from mahjong_ranking.models import SeasonRanking, KyuDanRanking
+
+MAIL_BODY = """
+Hallo! Ich bin's dein Server.
+
+Ich habe gerade die Mahjong Rankings als Excel exportiert und dachte mir das
+ich sie dir am besten gleich schicke.
+
+Bitte versuche nicht auf diese E-Mail zu antworten.
+Ich bin nur ein dummes Programm.
+
+mit lieben Grüßen
+
+Der Kasu Server
+"""
+
+
+def export_season_rankings(workbook, until):
+ SeasonRanking.objects.update(until=until)
+ season = until.year if until else date.today().year
+ object_list = SeasonRanking.objects.season_rankings()
+ title = "Mahjong Ladder - {}".format(season)
+ columns_settings = (
+ {'col': 'A', 'title': 'Rang', 'attr': 'placement', 'style': 'Integer',
+ 'width': 8},
+ {'col': 'B', 'title': 'Spitzname', 'attr': 'user.username',
+ 'style': 'Content',
+ 'width': 25},
+ {'col': 'C', 'title': '⌀ Platz', 'attr': 'avg_placement',
+ 'style': 'Float', 'width': 8},
+ {'col': 'D', 'title': '⌀ Punkte', 'attr': 'avg_score',
+ 'style': 'Float', 'width': 12},
+ {'col': 'E', 'title': 'Hanchans', 'attr': 'hanchan_count',
+ 'style': 'Integer', 'width': 10},
+ {'col': 'F', 'title': 'Gut', 'attr': 'good_hanchans',
+ 'style': 'Integer', 'width': 5},
+ {'col': 'G', 'title': 'Gewonnen', 'attr': 'won_hanchans',
+ 'style': 'Integer', 'width': 10},
+ )
+ workbook.generate_sheet(
+ title=title,
+ columns_settings=columns_settings,
+ object_list=object_list)
+
+
+def export_kyu_dan_rankings(workbook, until):
+ KyuDanRanking.objects.update(until=until)
+ object_list = KyuDanRanking.objects.all()
+ title = "Kyū & Dan Rankings"
+ columns_settings = (
+ {'col': 'A', 'title': 'Spitzname', 'attr': 'user.username',
+ 'style': 'Content', 'width': 14},
+ {'col': 'B', 'title': 'Voller Name', 'attr': 'user.full_name',
+ 'style': 'Content', 'width': 20},
+ {'col': 'C', 'title': 'Rang', 'attr': 'rank',
+ 'style': 'Content', 'width': 8},
+ {'col': 'D', 'title': 'Punkte', 'attr': 'points',
+ 'style': 'Integer', 'width': 8},
+ {'col': 'E', 'title': 'Hanchans', 'attr': 'hanchan_count',
+ 'style': 'Integer', 'width': 10},
+ {'col': 'F', 'title': 'Gut', 'attr': 'good_hanchans',
+ 'style': 'Integer', 'width': 5},
+ {'col': 'G', 'title': 'Gewonnen', 'attr': 'won_hanchans',
+ 'style': 'Integer', 'width': 10},
+ {'col': 'H', 'title': 'letzte Hanchan', 'attr': 'last_hanchan_date',
+ 'style': 'Date', 'width': 16},
+ )
+ workbook.generate_sheet(
+ title=title,
+ columns_settings=columns_settings,
+ object_list=object_list)
+
+
+class Command(BaseCommand):
+ """Exports the SeasonRankings"""
+ filename = str()
+ until = datetime
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ '--until', nargs='?', type=parse_date,
+ default=date.today(), metavar='YYYY-MM-DD',
+ help='Calculate and export rankings until the given date.')
+ parser.add_argument(
+ '--mail', nargs='*', type=str, metavar='user@example.com',
+ help='Send the spreadsheet via eMail to the given recipient.')
+
+ def handle(self, *args, **options):
+ """Exports the current ladder ranking in a spreadsheet.
+ This is useful as a backup in form of a hardcopy."""
+ self.until = timezone.make_aware(datetime.combine(
+ options['until'], time(23, 59, 59)
+ ))
+
+ self.filename = os.path.join(
+ settings.RANKING_EXPORT_PATH,
+ 'mahjong_rankings_{:%Y-%m-%d}.xlsx'.format(self.until)
+ )
+ workbook = xlsx.Workbook()
+ export_season_rankings(workbook, until=self.until)
+ export_kyu_dan_rankings(workbook, until=self.until)
+ os.makedirs(settings.RANKING_EXPORT_PATH, exist_ok=True)
+ workbook.save(self.filename)
+ if options['mail']:
+ self.send_mail(options['mail'])
+
+ def send_mail(self, recipients):
+ mail = EmailMessage(
+ subject='Mahjong Rankings vom {:%d.%m.%Y}'.format(self.until),
+ body=MAIL_BODY,
+ from_email=settings.DEFAULT_FROM_EMAIL,
+ to=recipients)
+ mail.attach_file(self.filename)
+ mail.send()
diff --git a/src/mahjong_ranking/management/commands/update_ranking.py b/src/mahjong_ranking/management/commands/updateranking.py
similarity index 54%
rename from src/mahjong_ranking/management/commands/update_ranking.py
rename to src/mahjong_ranking/management/commands/updateranking.py
index 4d6c1c5..546c78f 100644
--- a/src/mahjong_ranking/management/commands/update_ranking.py
+++ b/src/mahjong_ranking/management/commands/updateranking.py
@@ -4,30 +4,39 @@
Recalculate Mahjong Rankings...
"""
-from django.core.management.base import BaseCommand
from datetime import date, datetime, time
-from mahjong_ranking import models
-from django.utils.dateparse import parse_date
+
+from django.core.management.base import BaseCommand
from django.utils import timezone
+from django.utils.dateparse import parse_date
+
+from mahjong_ranking import models
+
class Command(BaseCommand):
""" Recalculate all Kyu/Dan Rankings """
- help = "Recalculate all Kyu/Dan Rankings"
+ help = "Recalculate the Kyu/Dan Rankings"
def add_arguments(self, parser):
- parser.add_argument('--since', nargs='?', type=parse_date)
- parser.add_argument('--until', nargs='?', type=parse_date)
- parser.add_argument('--forcerecalc', action='store_true')
+ parser.add_argument('-s', '--since', nargs='?', type=parse_date,
+ metavar='YYYY-MM-DD',
+ help='Use all Hanchans since the given date.')
+ parser.add_argument('-u', '--until', nargs='?', type=parse_date,
+ metavar='YYYY-MM-DD',
+ help='Only use Hanchans until the given date.')
+ parser.add_argument('-f', '--force', action='store_true',
+ help="Force the recalculation of all Hanchans.")
def handle(self, *args, **options):
since = options.get('since', None)
until = options.get('until', None)
- force_recalc = options.get('forcerecalc')
+ force_recalc = options.get('force')
if isinstance(since, date):
since = datetime.combine(since, time(0, 0, 0))
since = timezone.make_aware(since)
if isinstance(until, date):
until = datetime.combine(until, time(23, 59, 59))
until = timezone.make_aware(until)
- models.KyuDanRanking.objects.update(since=since, until=until, force_recalc=force_recalc)
+ models.KyuDanRanking.objects.update(since=since, until=until,
+ force_recalc=force_recalc)
diff --git a/src/mahjong_ranking/models.py b/src/mahjong_ranking/models.py
index b5d12d6..aa44006 100644
--- a/src/mahjong_ranking/models.py
+++ b/src/mahjong_ranking/models.py
@@ -362,11 +362,22 @@ class KyuDanRanking(models.Model):
verbose_name = _(u'Kyū/Dan Ranking')
verbose_name_plural = _(u'Kyū/Dan Rankings')
+ @property
+ def rank(self):
+ if self.dan is not None:
+ return "{0:d}. Dan".format(self.dan)
+ else:
+ return "{0:d}. Kyū".format(self.kyu or 10)
+
+ @property
+ def points(self):
+ return self.dan_points if self.dan is not None else self.kyu_points
+
def __str__(self):
if self.dan is not None:
return u"%s - %d. Dan" % (self.user.username, self.dan or 1)
else:
- return u"%s - %d. Kyu" % (self.user.username, self.kyu or 10)
+ return u"%s - %d. Kyū" % (self.user.username, self.kyu or 10)
def append_3_in_a_row_bonuspoints(self, hanchan):
u"""
@@ -382,7 +393,6 @@ class KyuDanRanking(models.Model):
self.wins_in_a_row = 0
return
if self.wins_in_a_row >= 3 and self.dan < 9:
-
LOGGER.info(
'adding bonuspoints for 3 wins in a row for %s', self.user)
new_dan_rank = self.dan + 1
diff --git a/src/mahjong_ranking/templates/mahjong_ranking/player_dan_score.html b/src/mahjong_ranking/templates/mahjong_ranking/player_dan_score.html
index 2ef6db7..82aba0f 100755
--- a/src/mahjong_ranking/templates/mahjong_ranking/player_dan_score.html
+++ b/src/mahjong_ranking/templates/mahjong_ranking/player_dan_score.html
@@ -50,6 +50,10 @@
{% endfor %}
+
+{% if kyu_dan_ranking.legacy_date %}
+Frühere Dan Punkte vom {{ kyu_dan_ranking.legacy_date|date }}: {{kyu_dan_ranking.legacy_dan_points }}
+{% endif %}
{% endblock %}
{% block buttonbar %}
diff --git a/src/mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html b/src/mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html
index 7da226c..8e31486 100755
--- a/src/mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html
+++ b/src/mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html
@@ -35,8 +35,12 @@
{% if perms.mahjong_ranking.change_hanchan %}
{% endif %}
-
+
-{% endfor %}
+{% endfor %}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
+
+{% block buttonbar %}
+ Download
+{% endblock %}
diff --git a/src/mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html b/src/mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html
index 2444be7..a24677b 100755
--- a/src/mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html
+++ b/src/mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html
@@ -16,7 +16,7 @@
{% trans 'Placement' %}
{% trans 'Players' %}
{% trans 'Kyu Points' %}
-
+
1.
@@ -45,6 +45,10 @@
{% endif %}
-{% endfor %}
+{% endfor %}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
+
+{% block buttonbar %}
+ Download
+{% endblock %}
diff --git a/src/mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html b/src/mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html
index 6558fe0..e4aaa30 100755
--- a/src/mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html
+++ b/src/mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html
@@ -72,5 +72,8 @@
+{% endblock %}
-{% endblock %}
\ No newline at end of file
+{% block buttonbar %}
+ Download
+{% endblock %}
diff --git a/src/mahjong_ranking/views.py b/src/mahjong_ranking/views.py
index 3580dbb..275e99c 100644
--- a/src/mahjong_ranking/views.py
+++ b/src/mahjong_ranking/views.py
@@ -12,6 +12,7 @@ from django.utils.translation import ugettext as _
from django.views import generic
from events.mixins import EventDetailMixin
+from kasu import xlsx
from . import forms, models
from .mixins import MahjongMixin
@@ -28,56 +29,6 @@ KYU_DAN_ORDER = { # map sort URL args to Django ORM order_by args
'-username': ('-user__username',)
}
-def getattr_recursive(obj, attr_string):
- attr_list = attr_string.split('.')
- return_value=None
- for attr in attr_list:
- print("Obj:", obj,'Attr:', attr)
- return_value = getattr(obj, attr)
- obj = return_value
- return return_value
-
-def generate_sheet(workbook, title, columns_settings, object_list):
- row = 1
- ws = workbook.create_sheet()
- ws.title = title
- ws.syncHorizontal = True
- ws.filterMode = True
-
- # setup print orientation
- ws.page_setup.orientation = ws.ORIENTATION_PORTRAIT
- ws.page_setup.paperSize = ws.PAPERSIZE_A4
- ws.page_setup.fitToWidth = True
- ws.print_options.horizontalCentered = True
-
- # setup page header
- ws.oddHeader.left.text = title
- ws.oddHeader.left.size = 14
- ws.oddHeader.left.font = "Amerika Sans"
- ws.oddHeader.left.color = "000000"
-
- ws.oddHeader.right.text = str(date.today())
- ws.oddHeader.right.size = 14
- ws.oddHeader.right.font = "Amerika Sans"
- ws.oddHeader.right.color = "000000"
-
- # write table header
- for column, data in enumerate(columns_settings, 1):
- cell = ws.cell(column=column, row=row, value=data['title'])
- cell.style = 'heading'
-
- # write the table content
- for line in object_list:
- row += 1
- for column, settings in enumerate(columns_settings, 1):
- cell = ws.cell(column=column, row=row, value=getattr_recursive(line, settings['attr']))
- cell.style = settings['style']
-
- # set column widths
- for settings in columns_settings:
- ws.column_dimensions[settings['col']].width = settings['width']
-
-
class DeleteHanchan(EventDetailMixin, PermissionRequiredMixin,
generic.DeleteView):
@@ -235,48 +186,67 @@ class PlayerScore(LoginRequiredMixin, generic.ListView):
return context
def get_xlsx(self, request, *args, **kwargs):
- from management.commands.export_ranking import geneate_excel
self.object_list = self.get_queryset()
- allow_empty = self.get_allow_empty()
response = django.http.HttpResponse(
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
- response[
- 'Content-Disposition'] = 'attachment; filename="{xlsx_filename}"'.format(xlsx_filename=self.xlsx_filename)
- xlxs_workbook = geneate_excel()
- print(self.xlsx_columns)
- generate_sheet(xlxs_workbook,
- title=self.xlsx_filename,
- columns_settings=self.xlsx_columns,
- object_list=self.object_list
- )
- xlxs_workbook.create_sheet()
+ response['Content-Disposition'] = 'attachment; ' \
+ 'filename="{xlsx_filename}"'.format(
+ xlsx_filename=self.xlsx_filename)
+ xlxs_workbook = xlsx.Workbook()
+ xlxs_workbook.generate_sheet(
+ title=self.xlsx_filename.split('.')[0],
+ columns_settings=self.xlsx_columns,
+ object_list=self.object_list
+ )
xlxs_workbook.save(response)
return response
class PlayerDanScore(PlayerScore):
template_name = 'mahjong_ranking/player_dan_score.html'
- xlsx_columns = (
- {'col': 'A', 'title': 'Beginn', 'attr': 'start', 'style': 'date', 'width': 14},
- {'col': 'B', 'title': 'Platzierung', 'attr': 'placement', 'style': 'int', 'width': 8},
- {'col': 'C', 'title': 'Spieler 1', 'attr': 'player1.username', 'style': 'content', 'width': 8},
- {'col': 'D', 'title': 'Punkte', 'attr': 'player1_game_score', 'style': 'int', 'width': 8},
- {'col': 'E', 'title': 'Spieler 2', 'attr': 'player2.username', 'style': 'content', 'width': 8},
- {'col': 'F', 'title': 'Punkte', 'attr': 'player2_game_score', 'style': 'int', 'width': 8},
- {'col': 'G', 'title': 'Spieler 3', 'attr': 'player3.username', 'style': 'content', 'width': 8},
- {'col': 'H', 'title': 'Punkte', 'attr': 'player3_game_score', 'style': 'int', 'width': 8},
- {'col': 'I', 'title': 'Spieler 4', 'attr': 'player4.username', 'style': 'content', 'width': 8},
- {'col': 'J', 'title': 'Punkte', 'attr': 'player4_game_score', 'style': 'int', 'width': 8},
- {'col': 'K', 'title': 'Dan Punkte', 'attr': 'dan_points', 'style': 'int', 'width': 8},
- {'col': 'L', 'title': 'Anmerkung', 'attr': 'comment', 'style': 'content', 'width': 8},
- )
def get_queryset(self):
- kyu_dan_ranking = models.KyuDanRanking.objects.get(user=self.user)
- self.xlsx_filename = "{username}_dan_score.xlsx".format(
- username=self.user.username)
- return models.Hanchan.objects.dan_hanchans(user=self.user,
- since=kyu_dan_ranking.legacy_date)
+ self.kyu_dan_ranking = models.KyuDanRanking.objects.get(user=self.user)
+ return models.Hanchan.objects.dan_hanchans(
+ user=self.user,
+ since=self.kyu_dan_ranking.legacy_date)
+
+ @property
+ def xlsx_columns(self):
+ return (
+ {'col': 'A', 'title': 'Beginn', 'attr': 'start',
+ 'style': 'Date Time',
+ 'width': 14, 'footer': self.kyu_dan_ranking.legacy_date},
+ {'col': 'B', 'title': 'Termin', 'attr': 'event.name',
+ 'style': 'Content', 'width': 16},
+ {'col': 'C', 'title': 'Platzierung', 'attr': 'placement',
+ 'style': 'Integer', 'width': 11},
+ {'col': 'D', 'title': 'Spieler 1', 'attr': 'player1.username',
+ 'style': 'Content', 'width': 16},
+ {'col': 'E', 'title': 'Punkte', 'attr': 'player1_game_score',
+ 'style': 'Integer', 'width': 8},
+ {'col': 'F', 'title': 'Spieler 2', 'attr': 'player2.username',
+ 'style': 'Content', 'width': 16},
+ {'col': 'G', 'title': 'Punkte', 'attr': 'player2_game_score',
+ 'style': 'Integer', 'width': 8},
+ {'col': 'H', 'title': 'Spieler 3', 'attr': 'player3.username',
+ 'style': 'Content', 'width': 16},
+ {'col': 'I', 'title': 'Punkte', 'attr': 'player3_game_score',
+ 'style': 'Integer', 'width': 8},
+ {'col': 'J', 'title': 'Spieler 4', 'attr': 'player4.username',
+ 'style': 'Content', 'width': 16},
+ {'col': 'K', 'title': 'Punkte', 'attr': 'player4_game_score',
+ 'style': 'Integer', 'width': 8},
+ {'col': 'L', 'title': 'Dan Punkte', 'attr': 'dan_points',
+ 'style': 'Integer', 'width': 12,
+ 'footer': self.kyu_dan_ranking.legacy_dan_points},
+ {'col': 'M', 'title': 'Anmerkung', 'attr': 'comment',
+ 'style': 'Content', 'width': 20, 'footer': 'Legacy Dan Punkte'},
+ )
+
+ @property
+ def xlsx_filename(self):
+ return "{username}_dan_score.xlsx".format(username=self.user.username)
class PlayerInvalidScore(PlayerScore):
@@ -292,9 +262,47 @@ class PlayerKyuScore(PlayerScore):
template_name = 'mahjong_ranking/player_kyu_score.html'
def get_queryset(self):
- self.xlsx_filename = "{username}_kyu_score.xlsx".format(
- username=self.user.username)
- return models.Hanchan.objects.kyu_hanchans(self.user)
+ self.kyu_dan_ranking = models.KyuDanRanking.objects.get(user=self.user)
+ return models.Hanchan.objects.kyu_hanchans(
+ user=self.user,
+ since=self.kyu_dan_ranking.legacy_date)
+
+ @property
+ def xlsx_columns(self):
+ return (
+ {'col': 'A', 'title': 'Beginn', 'attr': 'start',
+ 'style': 'Date Time',
+ 'width': 14, 'footer': self.kyu_dan_ranking.legacy_date},
+ {'col': 'B', 'title': 'Termin', 'attr': 'event.name',
+ 'style': 'Content', 'width': 16},
+ {'col': 'C', 'title': 'Platzierung', 'attr': 'placement',
+ 'style': 'Integer', 'width': 11},
+ {'col': 'D', 'title': 'Spieler 1', 'attr': 'player1.username',
+ 'style': 'Content', 'width': 16},
+ {'col': 'E', 'title': 'Punkte', 'attr': 'player1_game_score',
+ 'style': 'Integer', 'width': 8},
+ {'col': 'F', 'title': 'Spieler 2', 'attr': 'player2.username',
+ 'style': 'Content', 'width': 16},
+ {'col': 'G', 'title': 'Punkte', 'attr': 'player2_game_score',
+ 'style': 'Integer', 'width': 8},
+ {'col': 'H', 'title': 'Spieler 3', 'attr': 'player3.username',
+ 'style': 'Content', 'width': 16},
+ {'col': 'I', 'title': 'Punkte', 'attr': 'player3_game_score',
+ 'style': 'Integer', 'width': 8},
+ {'col': 'J', 'title': 'Spieler 4', 'attr': 'player4.username',
+ 'style': 'Content', 'width': 16},
+ {'col': 'K', 'title': 'Punkte', 'attr': 'player4_game_score',
+ 'style': 'Integer', 'width': 8},
+ {'col': 'L', 'title': 'Kyū Punkte', 'attr': 'kyu_points',
+ 'style': 'Integer', 'width': 12,
+ 'footer': self.kyu_dan_ranking.legacy_kyu_points},
+ {'col': 'M', 'title': 'Anmerkung', 'attr': 'comment',
+ 'style': 'Content', 'width': 24, 'footer': 'Legacy Kyū Punkte'},
+ )
+
+ @property
+ def xlsx_filename(self):
+ return "{username}_kyu_score.xlsx".format(username=self.user.username)
class PlayerLadderScore(PlayerScore):
@@ -311,10 +319,44 @@ class PlayerLadderScore(PlayerScore):
def get_queryset(self, **kwargs):
self.season = int(self.request.GET.get('season', date.today().year))
- self.xlsx_filename = "{username}_ladder ({season}).xlsx".format(
- username=self.user.username, season=self.season)
hanchan_list = models.Hanchan.objects.season_hanchans(
user=self.user,
season=self.season
)
return hanchan_list
+
+ @property
+ def xlsx_columns(self):
+ return (
+ {'col': 'A', 'title': 'Beginn', 'attr': 'start',
+ 'style': 'Date Time', 'width': 14},
+ {'col': 'B', 'title': 'Termin', 'attr': 'event.name',
+ 'style': 'Content', 'width': 16},
+ {'col': 'C', 'title': 'Platzierung', 'attr': 'placement',
+ 'style': 'Integer', 'width': 11},
+ {'col': 'D', 'title': 'Spieler 1', 'attr': 'player1.username',
+ 'style': 'Content', 'width': 16},
+ {'col': 'E', 'title': 'Punkte', 'attr': 'player1_game_score',
+ 'style': 'Integer', 'width': 8},
+ {'col': 'F', 'title': 'Spieler 2', 'attr': 'player2.username',
+ 'style': 'Content', 'width': 16},
+ {'col': 'G', 'title': 'Punkte', 'attr': 'player2_game_score',
+ 'style': 'Integer', 'width': 8},
+ {'col': 'H', 'title': 'Spieler 3', 'attr': 'player3.username',
+ 'style': 'Content', 'width': 16},
+ {'col': 'I', 'title': 'Punkte', 'attr': 'player3_game_score',
+ 'style': 'Integer', 'width': 8},
+ {'col': 'J', 'title': 'Spieler 4', 'attr': 'player4.username',
+ 'style': 'Content', 'width': 16},
+ {'col': 'K', 'title': 'Punkte', 'attr': 'player4_game_score',
+ 'style': 'Integer', 'width': 8},
+ {'col': 'L', 'title': 'Punkte', 'attr': 'game_score',
+ 'style': 'Integer', 'width': 8},
+ )
+
+ @property
+ def xlsx_filename(self):
+ return "{username}_ladder_score_{season}.xlsx".format(
+ username=self.user.username,
+ season=self.season
+ )
diff --git a/src/maistar_ranking/migrations/0007_auto_20171214_1215.py b/src/maistar_ranking/migrations/0007_auto_20171214_1215.py
new file mode 100644
index 0000000..66feede
--- /dev/null
+++ b/src/maistar_ranking/migrations/0007_auto_20171214_1215.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.8 on 2017-12-14 11:15
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('maistar_ranking', '0006_auto_20171115_0653'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='game',
+ name='player1',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Spieler 1'),
+ ),
+ migrations.AlterField(
+ model_name='game',
+ name='player2',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Spieler 2'),
+ ),
+ migrations.AlterField(
+ model_name='game',
+ name='player3',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Spieler 3'),
+ ),
+ migrations.AlterField(
+ model_name='game',
+ name='player4',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Spieler 4'),
+ ),
+ migrations.AlterField(
+ model_name='game',
+ name='player5',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Spieler 5'),
+ ),
+ migrations.AlterField(
+ model_name='game',
+ name='player6',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Spieler 6'),
+ ),
+ migrations.AlterField(
+ model_name='ranking',
+ name='user',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
+ ),
+ ]
diff --git a/src/membership/models.py b/src/membership/models.py
index 7157c5a..7b746d2 100644
--- a/src/membership/models.py
+++ b/src/membership/models.py
@@ -216,6 +216,10 @@ class Membership(AbstractUser):
verbose_name = _('Membership')
verbose_name_plural = _('Memberships')
+ @property
+ def full_name(self):
+ return " ".join([self.last_name, self.first_name])
+
def __str__(self):
return self.username
diff --git a/src/membership/templates/membership/membership_detail.html b/src/membership/templates/membership/membership_detail.html
index d04e6ba..d1af67c 100644
--- a/src/membership/templates/membership/membership_detail.html
+++ b/src/membership/templates/membership/membership_detail.html
@@ -35,7 +35,10 @@
Mahjong
{% if kyu_dan_ranking.dan %}
- {{kyu_dan_ranking.dan}}. Dan: {{ kyu_dan_ranking.dan_points }} {% trans 'Points' %}
+
+ {{kyu_dan_ranking.dan}}. Dan: {{ kyu_dan_ranking.dan_points }} {% trans 'Points' %}
+ ({% trans 'Maximum' %}: {{ kyu_dan_ranking.max_dan_points }})
+
{% elif kyu_dan_ranking.kyu%}
{{kyu_dan_ranking.kyu}}. Kyu: {{ kyu_dan_ranking.kyu_points }} {% trans 'Points' %}
{% endif %}
From 192721452e423c4c557a6fb08261a2835d63b422 Mon Sep 17 00:00:00 2001
From: Xeniac
Date: Fri, 29 Dec 2017 10:03:08 +0100
Subject: [PATCH 17/19] =?UTF-8?q?*=20Kommentare=20wenn=20Dan/Kyu=20Punktab?=
=?UTF-8?q?z=C3=BCge=20verringert=20werden=20um=20nicht=20unter=20=20=200?=
=?UTF-8?q?=20zu=20fallen.=20*=20Neue=20Middleware=20die=20REMOTE=5FIP=20a?=
=?UTF-8?q?us=20dem=20X-Forward-For=20Header=20setzt.=20=20=20Damit=20funk?=
=?UTF-8?q?tioniert=20das=20Kommentarsystem=20nun=20auch=20hinter=20nginx.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
bin/sync.sh | 4 +-
src/kasu/settings.py | 1 +
src/kasu/static/css/kasu.css | 3175 ++++++++++++++++-
src/mahjong_ranking/admin.py | 2 -
.../management/commands/exportranking.py | 2 +-
src/mahjong_ranking/models.py | 4 +
src/mahjong_ranking/views.py | 2 +-
src/utils/middleware.py | 20 +-
8 files changed, 3199 insertions(+), 11 deletions(-)
diff --git a/bin/sync.sh b/bin/sync.sh
index 756f1ca..7a78c0d 100755
--- a/bin/sync.sh
+++ b/bin/sync.sh
@@ -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"
diff --git a/src/kasu/settings.py b/src/kasu/settings.py
index e38a68c..9af87a5 100644
--- a/src/kasu/settings.py
+++ b/src/kasu/settings.py
@@ -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',
]
diff --git a/src/kasu/static/css/kasu.css b/src/kasu/static/css/kasu.css
index 66d72b2..b889b3e 100644
--- a/src/kasu/static/css/kasu.css
+++ b/src/kasu/static/css/kasu.css
@@ -1 +1,3174 @@
-@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.4.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.4.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.4.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.4.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.4.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.4.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}@font-face{font-family:'Philosopher';font-weight:normal;font-style:normal;src:url('../fonts/philosopher.woff') format('woff'),url('../fonts/philosopher.ttf') format('truetype')}@font-face{font-family:'Amerika Sans';font-weight:normal;font-style:normal;src:url('../fonts/amerikasans.woff') format('woff'),url('../fonts/amerikasans.ttf') format('truetype')}a:hover{color:#a40000;text-decoration:underline}a:link{color:#204a87;font-weight:700;text-decoration:none}a:visited{color:#5c3566}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section{display:block}body{font:12pt Philosopher,Georgia,serif;line-height:1;vertical-align:baseline}button,a.button,#redbox a.button:link,#redbox a.button:visited{display:inline-block;padding:.25em;margin:.25em;color:#2e3436;background-color:#f9f9f9;background:linear-gradient(to bottom, #f9f9f9 5%, #e9e9e9 100%);border:1px solid #d3d7cf;border-radius:5px;box-shadow:inset 0 1px 0 0 #ffffff;font:bold 12pt Philosopher,sans-serif;text-decoration:none;text-shadow:1px 1px 1px #ffffff}div.tab_container{background-color:#fff;padding-top:1em}fieldset{border:none;color:#2e3436;border-radius:10px;padding:0 10px 0 160px;background-color:#f2f5f6;background:linear-gradient(135deg, #f2f5f6 0, #e3eaed 37%, #c8d7dc 100%);vertical-align:top;margin-bottom:1em}fieldset div{margin:5px 0}fieldset legend{margin-top:-0.1em;margin-left:-150px;color:#a40000;font-family:'Amerika Sans',sans-serif;font-variant:small-caps;font-weight:400;font-size:16pt;text-shadow:2px 2px 2px #888}fieldset .required{font-weight:bold}fieldset .buttonbar{border-radius:0 0 10px 10px;margin:0 -10px 0 -160px}fieldset .help_text{font-size:small}fieldset .field_name{text-align:right;width:140px;margin:0 20px 0 -160px;padding-top:3px;display:inline-block;clear:left;vertical-align:top}fieldset input,fieldset textarea{border:1px solid #999999;border-radius:5px;padding:2px;margin:0}fieldset input[maxlength="255"],fieldset textarea{box-sizing:border-box;width:100%;max-width:760px}fieldset ul{display:inline-block;padding:0}fieldset ul li{list-style:none;display:inline}fieldset table{display:inline-table;max-width:760px}fieldset.comment{padding:0}fieldset.comment legend{margin-left:15px}fieldset.comment .buttonbar{margin:0;width:100%}h1,h2,h3,h4,h5,h6,.player{color:#bc0a19;font-family:'Amerika Sans',sans-serif;font-variant:small-caps;font-weight:400;letter-spacing:-1px;margin:1em 0 .5em 0;text-shadow:1px 1px 1px #888;vertical-align:baseline}h1 a:link,h2 a:link,h3 a:link,h4 a:link,h5 a:link,h6 a:link,.player a:link,h1 a:visited,h2 a:visited,h3 a:visited,h4 a:visited,h5 a:visited,h6 a:visited,.player a:visited{color:#bc0a19;font-weight:400;text-decoration:none}ol{list-style:cjk-ideographic;padding-left:2em}table{border-collapse:collapse;border-spacing:0;width:100%;margin:0 auto 1em auto}table td{padding:2px;vertical-align:middle}table th{background:#a40000;color:#fff;padding:2px;vertical-align:middle}table th a:link,table th a:visited{color:#FFF}table tr:nth-child(2n+1){background-color:rgba(176,176,174,0.25)}table tr:hover{background-color:rgba(164,0,0,0.25)}ul{list-style:circle outside;padding-left:2em;margin-top:.5em}ul li{margin-bottom:.5em}ul.info{list-style:none;margin:0 0 .5em 0;padding-left:0}ul.info li{display:inline-block;margin-right:1em}ul.tabs{width:100%;list-style:none;margin:1em 0 0 0;padding:.5em 0 0 0;border-radius:10px 10px 0 0;background-color:#bc0a19;background:linear-gradient(to bottom, #000 0, #45484d 100%)}ul.tabs li{background-color:#fa665a;background:linear-gradient(to bottom, #fa665a 5%, #d34639 100%);border-radius:5px 15px 0 0;display:inline-block;margin:0 -5px 0 10px}ul.tabs li a{color:#ffffff;text-shadow:1px 1px 1px #2e3436;display:inline-block;padding:.5em 1em;text-decoration:none}ul.tabs li.active{background:#fff}ul.tabs li.active a{color:#bc0a19;text-decoration:none;text-shadow:1px 1px 1px #888}#bottom_buttonbar{border-radius:0 0 10px 10px}#display{position:relative;text-align:center}#redbox{color:white;background-color:#a90329;background:linear-gradient(135deg, #a90329 0, #8f0222 44%, #6d0019 100%);border-radius:10px;padding:10px}#redbox h2,#redbox h3,#redbox a:link,#redbox a:visited{color:white;font-weight:normal;text-shadow:1px 1px 1px #000}#redbox h2:first-of-type{margin:-20px 0 0 10px;color:black;text-shadow:1px 1px 1px #ffffff}.avatar{display:block;position:relative;float:left;width:70px;height:70px;padding:0;margin:0 1em 1em 0;box-shadow:2px 2px 5px #888}.avatar img{height:70px;width:70px}.buttonbar{text-align:right;border-radius:10px;background-color:#000000;background:linear-gradient(to bottom, #45484d 0, #000000 100%)}.center{text-align:center}.clear,.clearfix{clear:both}.cke_chrome{border:0 !important}.cke_wysiwyg_div{padding:1em 0 !important}.django-ckeditor-widget{width:100%;min-height:500px}.disabled{color:#ccc}.comment{display:table;margin-bottom:1em;width:100%;padding:0}.error,ul.errorlist li{color:#a40000}.game h2{margin:.5em 0}.grid_1,.grid_2,.grid_3,.grid_4,.grid_5,.grid_6,.grid_7,.grid_8,.grid_9,.grid_10,.grid_11,.grid_12{display:inline;float:left;margin:0 10px;position:relative;box-sizing:border-box}.grid_1{width:60px}.grid_2{width:140px}.grid_3{width:220px}.grid_4{width:300px}.grid_5{width:380px}.grid_6{width:460px}.grid_7{width:540px}.grid_8{width:620px}.grid_9{width:700px}.grid_10{width:780px}.grid_11{width:860px}.grid_12{width:940px}.more_link{text-align:right;clear:left}.pagination{clear:both;margin-bottom:1em;padding:0;position:relative;text-align:center;z-index:30}.pagination a{background-color:#000000;background:linear-gradient(to bottom, #45484d 0, #000000 100%);border-radius:5px;display:inline-block;font-weight:bold;padding:.5em 1em;text-decoration:none}.pagination a:link,.pagination a:visited{color:#fff}.pagination a:hover,.pagination a.active{color:#bc0a19}.pagination a.disabled,.pagination a.disabled:hover{color:#666}.pagination a.previous{float:left;border-radius:10px 5px 5px 10px}.pagination a.next{float:right;border-radius:5px 10px 10px 5px}.player{margin:0 auto;float:none}.right{text-align:right}.thumbnail{display:block;position:relative;float:left;height:140px;width:140px;padding:5px;border:0;margin:5px;background:transparent url('../img/thumbnail-bg.png') top left no-repeat}@media screen and (min-width:700px){#siteheader{min-height:110px;width:960px;margin:0 auto;padding:0;position:relative;z-index:50}#sitelogo{background:url(../img/logo.png) no-repeat;height:110px;margin:0;padding:0;position:absolute;top:5px;left:5px;text-indent:-9999px;width:233px;z-index:99}#sitelogo a{display:block;height:110px;left:0;position:absolute;top:0;width:233px}#mainnav{left:233px;position:absolute;bottom:0}#mainnav #toggle,#mainnav .toggle{display:none}#mainnav ul.main_menu{list-style:none;margin:0;padding:0}#mainnav ul.main_menu>li{display:inline-block;min-width:50px;padding:8px;font:normal small-caps 18px 'Amerika Sans',sans-serif;text-align:center;text-shadow:2px 2px 2px #2e3436;margin:0}#mainnav ul.main_menu>li a{color:#000;text-decoration:none;font-weight:normal}#mainnav ul.main_menu>li a.active{color:#bc0a19}#mainnav ul.main_menu>li a:hover{color:#FFF}#mainnav ul.main_menu>li:first-child{padding-left:0}#mainnav ul.main_menu>li:last-child{padding-right:0}#mainnav ul.main_menu>li>ul{display:none;background:rgba(255,255,255,0.8);border-radius:10px;padding:.25em;min-width:10em;position:absolute;top:100%;margin:0 0 0 -15px;box-shadow:-1px 1px 5px 0 rgba(0,0,0,0.75);transition:all .25s linear}#mainnav ul.main_menu>li>ul li{display:block;float:none;font:normal small-caps 14pt 'Amerika Sans',sans-serif;text-align:left}#mainnav ul.main_menu>li>ul li a{display:block;color:black;padding:5px;transition:all .25s linear;border-radius:.5em}#mainnav ul.main_menu>li>ul li a:hover{color:white;background:#bc0a19;background:linear-gradient(135deg, #a90329 0, #8f0222 44%, #6d0019 100%);transition:all .25s linear}#mainnav ul.main_menu li:hover>ul{display:block}}@media screen and (max-width:699px){body{margin:5px 10px;background:url('../img/background_mobile.png') no-repeat top center;background-attachment:fixed;font:12pt "Philosopher",Georgia,serif}img{max-width:100%;height:auto}img.thumbnail{display:block;float:left;height:70px;width:70px;margin:5px;box-shadow:2px 2px 5px #888}img.posting_image,img.partner{float:left;width:99px;height:59px;padding:2px;margin:1em .5em 0 0;border:1px solid #babdb6}ul.main_dropdown{list-style-type:none;margin:0;padding:0}ul.main_dropdown li{padding:0}ul.main_dropdown a{padding-left:2em;font-size:12pt}#display .grid_10{margin:0;position:relative;z-index:1}#display .next,#display .previous{display:block;position:absolute;top:0;width:45px;height:100%;margin:0;padding:0;text-indent:9999px;overflow:hidden;opacity:.5}#display .next:hover,#display .previous:hover{opacity:.9;transition:all .2s ease-out}#display .next{background:transparent url(../img/right-arrow.png) no-repeat center center;right:0;z-index:3}#display .previous{background:transparent url(../img/left-arrow.png) no-repeat center center;left:0;z-index:2}#footer{border-top:1px solid black;text-align:center}#jumbotron{background:none !important}#maincontent{width:100%}#mainnav{display:block;float:right}#navigation{margin:10px 0;padding:0;background:#45484d url("../img/navigation-mobile.png") top left repeat-x;background-size:contain}#navigation a{font:bold 12px Arial;color:#FFF;text-decoration:none}#navigation li{display:inline-block;padding:.5em .3em .5em .5em;text-align:center;border-left:1px solid #ffffff;margin:0}#navigation li:first-of-type{border:none}#sitelogo{background:url('../img/logo_mobile.png') no-repeat;width:114px;height:54px;left:5px;margin:0;padding:0;text-indent:-9999px;top:5px;z-index:20;float:left}#sitelogo a{display:block;width:114px;height:54px}#siteheader:after{content:"";clear:both;display:block;visibility:hidden;height:0}#redbox{margin-top:1em;display:block}#redbox h3{margin:.5em 0}#redbox ul li{margin-bottom:.75em}#teaser{background:none;margin-bottom:1em}#teaser_text{background:rgba(255,255,255,0.5)}#topnav a{display:inline-block;color:#000;font:400 small-caps 24pt 'Amerika Sans',sans-serif;min-width:80px;text-align:center;text-decoration:none;text-shadow:2px 2px 2px #2e3436;padding:5px}#topnav a.active{color:#bc0a19}#topnav a:hover{color:#FFF}#toggle,.toggle{display:none}#toggle:checked~.main_menu{display:block;opacity:1}#toggle:checked~.toggle,.toggle:hover{background:#45ABD6}#usernav a{display:inline-block;margin:0 .5em}.comment_picture{display:table-cell;padding:0 10px;width:60px;vertical-align:top}.comment_header{display:table-cell;padding:0 10px;width:140px;vertical-align:top}.comment_header h3{margin:0}.comment{display:block}.comment_picture{display:block;float:left;vertical-align:top;width:60px}.comment_header{display:block;float:left;padding:0 10px;vertical-align:top;width:140px}.comment_header h3{margin:0}.comment_text{border-top:1px solid #45484d;display:block;margin:0 10px;padding-top:.5em;clear:both}.gallery{float:left;width:150px;height:150px;margin:10px}.gallery h3{font-size:12pt}.game img{float:right;margin:.5em 0 .5em 1em;width:140px;height:auto;box-shadow:1px 1px 5px 1px #444}.game:nth-child(2n+1) img{float:left;margin-right:1em;margin:.5em 2em .5em 0}.grid_2{min-width:140px;width:31%;margin:1% 0 1% 0}.grid_3{width:48%;margin:1% 0 1% 0}.grid_4,.grid_5,.grid_6,.grid_7,.grid_8,.grid_9,.grid_10,.grid_11,.grid_12{clear:both;margin:0;width:100%}.main_menu{position:absolute;display:none;right:0;min-width:50%;z-index:999;background:#eeeeec;margin:0;padding:0;border-top:1px solid #a40000;border-bottom:3px solid #a40000}.main_menu li{display:block;list-style:none;margin:0}.main_menu li a{display:block;width:100%;text-decoration:none;font:400 small-caps 18px 'Amerika Sans',sans-serif;color:black;box-sizing:border-box;border-left:0 solid #eeeeec;padding:.5em 1em;line-height:1;transition:all .25s linear}.main_menu li a:hover,.main_menu li a:focus{color:#a40000;border-left:3px solid #a40000}.officer{float:left;width:25%;padding:5px;box-sizing:border-box;text-align:center}.officer img{border:0;border-radius:50%;box-shadow:1px 1px 5px 1px #444;width:100%;height:100%}.officer .function{font-size:small;margin-top:.25em}.toggle{background:#a40000;border-radius:5px;color:#FFFFFF;cursor:pointer;display:block;margin:8px 0;padding:10px;position:relative;transition:all .5s linear;z-index:2}.thumbnail{display:block;position:relative;float:left;height:70px;padding:0;width:70px;margin:5px;box-shadow:2px 2px 5px #888}.thumbnail img{height:70px;width:70px}}@media screen and (min-width:700px){body{position:relative;margin:0;padding:0;min-width:960px;height:100%}#body{background-color:#ffffff;background-image:url('../img/kranich.png'),url('../img/header_bg.jpg');background-repeat:no-repeat,no-repeat;background-position:center bottom,center top;background-attachment:scroll,fixed}#bottom_buttonbar{position:absolute;bottom:0;margin:0;width:100%}#content{width:940px;margin:0 10px;position:relative}#display .next,#display .previous{display:block;position:absolute;top:0;width:60px;height:100%;margin:0;padding:0;text-indent:9999px;overflow:hidden;opacity:.5}#display .next:hover,#display .previous:hover{opacity:1;transition:all .2s ease-out}#display .next{background:transparent url(../img/right-arrow.png) no-repeat center center;right:10px;z-index:3}#display .previous{background:transparent url(../img/left-arrow.png) no-repeat center center;left:10px;z-index:2}#display img{box-shadow:1px 1px 5px 1px #444}#footer{width:920px;min-height:50px;margin:20px auto 0 auto;z-index:30}#footer p{text-align:center}#google_maps{position:relative;top:0;left:0;height:280px;padding:10px;border-radius:0 10px 10px 0}#jumbotron{clear:both;position:relative;padding:0;width:940px;margin:0 10px 1em 10px;z-index:5;min-height:300px;border:none;border-radius:10px;background-repeat:no-repeat;background-color:#333;background-position:center left}#jumbotron>h2,#jumbotron>h1{color:#eff0ef;text-shadow:1px 1px 1px #000;position:absolute;top:33%;left:10px;max-width:600px;margin:0}#jumbotron #teaser_text{display:block;position:absolute;left:0;bottom:0;width:620px;color:#FFF;background:rgba(0,0,0,0.5);padding:1em;border-radius:0 0 0 10px}#jumbotron #teaser_text a:link,#jumbotron #teaser_text a:active,#jumbotron #teaser_text a:visited{color:#fff;text-decoration:underline}#maincontent{margin:0 auto;height:auto !important;width:960px;min-height:800px;padding:10px 0 2em 0;position:relative;z-index:19;border-radius:10px;background:rgba(255,255,255,0.5);box-shadow:0 0 20px 1px rgba(0,0,0,0.75)}#messages{clear:both;margin:0 auto;padding:8px 0 0 30px;width:920px;list-style:none}#messages li.success{color:#253324;background:#89bd84;border:1px solid #253324;border-radius:10px;margin:10px;padding:10px}#navigation{clear:both;background:url(../img/navigation-bg.png) no-repeat left top;height:56px;margin:0 auto;padding:8px 35px 0 25px;position:relative;width:900px;z-index:30}#navigation a{background:url(../img/navigation-separator.png) no-repeat right center;color:#FFF;display:inline-block;line-height:50px;font-weight:bold;height:50px;padding:0 15px;text-decoration:none}#navigation a:hover,#navigation a.active{background:url(../img/navigation-hover.png) repeat-x left top;color:#3B3B3B}#navigation li{display:inline}#recaptcha_widget_div{margin-top:-20px}#redbox{position:absolute;top:0;right:0;height:280px;width:280px;border-radius:0 10px 10px 0;z-index:100}#redbox h3{margin:.5em 0}#redbox ul li{line-height:1em;margin-bottom:.5em}#usernav{position:absolute;top:0;right:0;text-align:right;background:black;background:linear-gradient(to bottom, #45484d 0, #000000 100%);border-radius:0 0 0 10px;font-size:14pt;color:#FFF;padding:10px 10px;z-index:50;box-shadow:-1px -1px 5px 1px rgba(0,0,0,0.75)}#usernav a{color:#FFF}.comment_picture{display:table-cell;padding:0 10px;width:60px;vertical-align:top}.comment_header{display:table-cell;padding:0 10px;width:140px;vertical-align:top}.comment_header h3{margin:0}.comment_text{display:table-cell;padding:0 10px;width:auto;max-width:700px}.gallery{display:inline;float:left;height:200px;margin:10px;overflow:hidden;text-align:center;width:300px}.gallery .thumbnail{display:block;float:none;margin:5px auto}.game img{float:right;margin:.5em 0 .5em 1em;width:300px;height:auto;box-shadow:1px 1px 5px 1px #444}.game:nth-child(2n+1) img{float:left;margin-right:1em;margin:.5em 2em .5em 0}.officer{float:left;width:140px;margin:5px;box-sizing:border-box;text-align:center}.officer img{border:0;border-radius:50%;box-shadow:1px 1px 5px 1px #444;width:130px;height:130px}.officer .function{font-size:small;margin-top:.25em}img.posting_image,img.partner{float:left;width:200px;height:120px;padding:2px;margin:0 1em 1em 0;border:1px solid #babdb6}.thumbnail a.delete_image{position:absolute;right:4px;bottom:0}}@page{margin:1cm 1cm 1cm 2cm;size:A4 portrait}@media print{a:link,a:visited{color:black;font-weight:bold}body,article{width:100%;margin:0;padding:0;color:#000;background:#fff}h1{font-size:32pt}h2,h3,h4,h5,h6{text-shadow:none;page-break-after:avoid}img{max-width:100% !important;page-break-inside:avoid}nav,aside{display:none}ul{page-break-inside:avoid}#footer{width:100%;padding-top:.5em;border-top:1px solid black;text-align:center}#jumbotron{background:none !important}#maincontent nav{display:none}#maincontent aside{display:none}#sitelogo{background:url(../img/logo.png) top right no-repeat;background-size:contain;left:0;margin:0;padding:0;line-height:1cm;font-family:'Amerika Sans',Helvetica;font-size:8pt;top:5px;z-index:99}#comment_form,#comments,#footer,#navigation,#mainnav,#usernav,#bottom_buttonbar,#footer>form{display:none}.grid_6,grid_7,.grid_8,grid_9,.grid_10,.grid_11,.grid_12{width:100%}.more_link{display:none}}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}
\ No newline at end of file
+/* FONT PATH
+ * -------------------------- */
+@font-face {
+ font-family: 'FontAwesome';
+ src: url('../fonts/fontawesome-webfont.eot?v=4.4.0');
+ src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.4.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.4.0') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.4.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.4.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.4.0#fontawesomeregular') format('svg');
+ font-weight: normal;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'Philosopher';
+ font-weight: normal;
+ font-style: normal;
+ src: url('../fonts/philosopher.woff') format('woff'), url('../fonts/philosopher.ttf') format('truetype');
+}
+@font-face {
+ font-family: 'Amerika Sans';
+ font-weight: normal;
+ font-style: normal;
+ src: url('../fonts/amerikasans.woff') format('woff'), url('../fonts/amerikasans.ttf') format('truetype');
+}
+a:hover {
+ color: #a40000;
+ text-decoration: underline;
+}
+a:link {
+ color: #204a87;
+ font-weight: 700;
+ text-decoration: none;
+}
+a:visited {
+ color: #5c3566;
+}
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+menu,
+nav,
+section {
+ display: block;
+}
+body {
+ font: 12pt Philosopher, Georgia, serif;
+ line-height: 1;
+ vertical-align: baseline;
+}
+button,
+a.button,
+#redbox a.button:link,
+#redbox a.button:visited {
+ display: inline-block;
+ padding: 0.25em;
+ margin: 0.25em;
+ color: #2e3436;
+ background-color: #f9f9f9;
+ background: linear-gradient(to bottom, #f9f9f9 5%, #e9e9e9 100%);
+ border: 1px solid #d3d7cf;
+ border-radius: 5px;
+ box-shadow: inset 0px 1px 0px 0px #ffffff;
+ font: bold 12pt Philosopher, sans-serif;
+ text-decoration: none;
+ text-shadow: 1px 1px 1px #ffffff;
+}
+div.tab_container {
+ background-color: #fff;
+ padding-top: 1em;
+}
+fieldset {
+ border: none;
+ color: #2e3436;
+ border-radius: 10px;
+ padding: 0 10px 0 160px;
+ background-color: #f2f5f6;
+ background: linear-gradient(135deg, #f2f5f6 0%, #e3eaed 37%, #c8d7dc 100%);
+ vertical-align: top;
+ margin-bottom: 1em;
+}
+fieldset div {
+ margin: 5px 0px;
+}
+fieldset legend {
+ margin-top: -0.1em;
+ margin-left: -150px;
+ color: #a40000;
+ font-family: 'Amerika Sans', sans-serif;
+ font-variant: small-caps;
+ font-weight: 400;
+ font-size: 16pt;
+ text-shadow: 2px 2px 2px #888;
+}
+fieldset .required {
+ font-weight: bold;
+}
+fieldset .buttonbar {
+ border-radius: 0px 0px 10px 10px;
+ margin: 0 -10px 0 -160px;
+}
+fieldset .help_text {
+ font-size: small;
+}
+fieldset .field_name {
+ text-align: right;
+ width: 140px;
+ margin: 0 20px 0 -160px;
+ padding-top: 3px;
+ display: inline-block;
+ clear: left;
+ vertical-align: top;
+}
+fieldset input,
+fieldset textarea {
+ border: 1px solid #999999;
+ border-radius: 5px;
+ padding: 2px;
+ margin: 0;
+}
+fieldset input[maxlength="255"],
+fieldset textarea {
+ box-sizing: border-box;
+ width: 100%;
+ max-width: 760px;
+}
+fieldset ul {
+ display: inline-block;
+ padding: 0;
+}
+fieldset ul li {
+ list-style: none;
+ display: inline;
+}
+fieldset table {
+ display: inline-table;
+ max-width: 760px;
+}
+fieldset.comment {
+ padding: 0;
+}
+fieldset.comment legend {
+ margin-left: 15px;
+}
+fieldset.comment .buttonbar {
+ margin: 0;
+ width: 100%;
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+.player {
+ color: #bc0a19;
+ font-family: 'Amerika Sans', sans-serif;
+ font-variant: small-caps;
+ font-weight: 400;
+ letter-spacing: -1px;
+ margin: 1em 0 0.5em 0;
+ text-shadow: 1px 1px 1px #888;
+ vertical-align: baseline;
+}
+h1 a:link,
+h2 a:link,
+h3 a:link,
+h4 a:link,
+h5 a:link,
+h6 a:link,
+.player a:link,
+h1 a:visited,
+h2 a:visited,
+h3 a:visited,
+h4 a:visited,
+h5 a:visited,
+h6 a:visited,
+.player a:visited {
+ color: #bc0a19;
+ font-weight: 400;
+ text-decoration: none;
+}
+ol {
+ list-style: cjk-ideographic;
+ padding-left: 2em;
+}
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+ width: 100%;
+ margin: 0 auto 1em auto;
+}
+table td {
+ padding: 2px;
+ vertical-align: middle;
+}
+table th {
+ background: #a40000;
+ color: #fff;
+ padding: 2px;
+ vertical-align: middle;
+}
+table th a:link,
+table th a:visited {
+ color: #FFF;
+}
+table tr:nth-child(2n+1) {
+ background-color: rgba(176, 176, 174, 0.25);
+}
+table tr:hover {
+ background-color: rgba(164, 0, 0, 0.25);
+}
+ul {
+ list-style: circle outside;
+ padding-left: 2em;
+ margin-top: 0.5em;
+}
+ul li {
+ margin-bottom: 0.5em;
+}
+ul.info {
+ list-style: none;
+ margin: 0 0 0.5em 0;
+ padding-left: 0;
+}
+ul.info li {
+ display: inline-block;
+ margin-right: 1em;
+}
+ul.tabs {
+ width: 100%;
+ list-style: none;
+ margin: 1em 0 0 0;
+ padding: 0.5em 0 0 0;
+ border-radius: 10px 10px 0 0;
+ background-color: #bc0a19;
+ background: linear-gradient(to bottom, #000 0%, #45484d 100%);
+}
+ul.tabs li {
+ background-color: #fa665a;
+ background: linear-gradient(to bottom, #fa665a 5%, #d34639 100%);
+ border-radius: 5px 15px 0 0;
+ display: inline-block;
+ margin: 0px -5px 0px 10px;
+}
+ul.tabs li a {
+ color: #ffffff;
+ text-shadow: 1px 1px 1px #2e3436;
+ display: inline-block;
+ padding: 0.5em 1em;
+ text-decoration: none;
+}
+ul.tabs li.active {
+ background: #fff;
+}
+ul.tabs li.active a {
+ color: #bc0a19;
+ text-decoration: none;
+ text-shadow: 1px 1px 1px #888;
+}
+#bottom_buttonbar {
+ border-radius: 0 0 10px 10px;
+}
+#display {
+ position: relative;
+ text-align: center;
+}
+#redbox {
+ color: white;
+ background-color: #a90329;
+ background: linear-gradient(135deg, #a90329 0%, #8f0222 44%, #6d0019 100%);
+ border-radius: 10px;
+ padding: 10px;
+}
+#redbox h2,
+#redbox h3,
+#redbox a:link,
+#redbox a:visited {
+ color: white;
+ font-weight: normal;
+ text-shadow: 1px 1px 1px #000;
+}
+#redbox h2:first-of-type {
+ margin: -20px 0 0 10px;
+ color: black;
+ text-shadow: 1px 1px 1px #ffffff;
+}
+.avatar {
+ display: block;
+ position: relative;
+ float: left;
+ width: 70px;
+ height: 70px;
+ padding: 0;
+ margin: 0 1em 1em 0;
+ box-shadow: 2px 2px 5px #888;
+}
+.avatar img {
+ height: 70px;
+ width: 70px;
+}
+.buttonbar {
+ text-align: right;
+ border-radius: 10px;
+ background-color: #000000;
+ background: linear-gradient(to bottom, #45484d 0%, #000000 100%);
+}
+.center {
+ text-align: center;
+}
+.clear,
+.clearfix {
+ clear: both;
+}
+.cke_chrome {
+ border: 0 !important;
+}
+.cke_wysiwyg_div {
+ padding: 1em 0 !important;
+}
+.django-ckeditor-widget {
+ width: 100%;
+ min-height: 500px;
+}
+.disabled {
+ color: #ccc;
+}
+.comment {
+ display: table;
+ margin-bottom: 1em;
+ width: 100%;
+ padding: 0;
+}
+.error,
+ul.errorlist li {
+ color: #a40000;
+}
+.game h2 {
+ margin: 0.5em 0;
+}
+.grid_1,
+.grid_2,
+.grid_3,
+.grid_4,
+.grid_5,
+.grid_6,
+.grid_7,
+.grid_8,
+.grid_9,
+.grid_10,
+.grid_11,
+.grid_12 {
+ display: inline;
+ float: left;
+ margin: 0px 10px;
+ position: relative;
+ box-sizing: border-box;
+}
+.grid_1 {
+ width: 60px;
+}
+.grid_2 {
+ width: 140px;
+}
+.grid_3 {
+ width: 220px;
+}
+.grid_4 {
+ width: 300px;
+}
+.grid_5 {
+ width: 380px;
+}
+.grid_6 {
+ width: 460px;
+}
+.grid_7 {
+ width: 540px;
+}
+.grid_8 {
+ width: 620px;
+}
+.grid_9 {
+ width: 700px;
+}
+.grid_10 {
+ width: 780px;
+}
+.grid_11 {
+ width: 860px;
+}
+.grid_12 {
+ width: 940px;
+}
+.more_link {
+ text-align: right;
+ clear: left;
+}
+.pagination {
+ clear: both;
+ margin-bottom: 1em;
+ padding: 0;
+ position: relative;
+ text-align: center;
+ z-index: 30;
+}
+.pagination a {
+ background-color: #000000;
+ background: linear-gradient(to bottom, #45484d 0%, #000000 100%);
+ border-radius: 5px;
+ display: inline-block;
+ font-weight: bold;
+ padding: 0.5em 1em;
+ text-decoration: none;
+}
+.pagination a:link,
+.pagination a:visited {
+ color: #fff;
+}
+.pagination a:hover,
+.pagination a.active {
+ color: #bc0a19;
+}
+.pagination a.disabled,
+.pagination a.disabled:hover {
+ color: #666;
+}
+.pagination a.previous {
+ float: left;
+ border-radius: 10px 5px 5px 10px;
+}
+.pagination a.next {
+ float: right;
+ border-radius: 5px 10px 10px 5px;
+}
+.player {
+ margin: 0 auto;
+ float: none;
+}
+.right {
+ text-align: right;
+}
+.thumbnail {
+ display: block;
+ position: relative;
+ float: left;
+ height: 140px;
+ width: 140px;
+ padding: 5px;
+ border: 0;
+ margin: 5px;
+ background: transparent url('../img/thumbnail-bg.png') top left no-repeat;
+}
+@media screen and (min-width: 700px) {
+ #siteheader {
+ min-height: 110px;
+ width: 960px;
+ margin: 0 auto;
+ padding: 0;
+ position: relative;
+ z-index: 50;
+ }
+ #sitelogo {
+ background: url(../img/logo.png) no-repeat;
+ height: 110px;
+ margin: 0;
+ padding: 0;
+ position: absolute;
+ top: 5px;
+ left: 5px;
+ text-indent: -9999px;
+ width: 233px;
+ z-index: 99;
+ }
+ #sitelogo a {
+ display: block;
+ height: 110px;
+ left: 0;
+ position: absolute;
+ top: 0;
+ width: 233px;
+ }
+ #mainnav {
+ left: 233px;
+ position: absolute;
+ bottom: 0px;
+ }
+ #mainnav #toggle,
+ #mainnav .toggle {
+ display: none;
+ }
+ #mainnav ul.main_menu {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ }
+ #mainnav ul.main_menu > li {
+ display: inline-block;
+ min-width: 50px;
+ padding: 8px;
+ font: normal small-caps 18px 'Amerika Sans', sans-serif;
+ text-align: center;
+ text-shadow: 2px 2px 2px #2e3436;
+ margin: 0;
+ }
+ #mainnav ul.main_menu > li a {
+ color: #000;
+ text-decoration: none;
+ font-weight: normal;
+ }
+ #mainnav ul.main_menu > li a.active {
+ color: #bc0a19;
+ }
+ #mainnav ul.main_menu > li a:hover {
+ color: #FFF;
+ }
+ #mainnav ul.main_menu > li:first-child {
+ padding-left: 0;
+ }
+ #mainnav ul.main_menu > li:last-child {
+ padding-right: 0;
+ }
+ #mainnav ul.main_menu > li > ul {
+ display: none;
+ background: rgba(255, 255, 255, 0.8);
+ border-radius: 10px;
+ padding: 0.25em;
+ min-width: 10em;
+ position: absolute;
+ top: 100%;
+ margin: 0 0 0 -15px;
+ box-shadow: -1px 1px 5px 0px rgba(0, 0, 0, 0.75);
+ transition: all 0.25s linear;
+ }
+ #mainnav ul.main_menu > li > ul li {
+ display: block;
+ float: none;
+ font: normal small-caps 14pt 'Amerika Sans', sans-serif;
+ text-align: left;
+ }
+ #mainnav ul.main_menu > li > ul li a {
+ display: block;
+ color: black;
+ padding: 5px;
+ transition: all 0.25s linear;
+ border-radius: 0.5em;
+ }
+ #mainnav ul.main_menu > li > ul li a:hover {
+ color: white;
+ background: #bc0a19;
+ background: linear-gradient(135deg, #a90329 0%, #8f0222 44%, #6d0019 100%);
+ transition: all 0.25s linear;
+ }
+ #mainnav ul.main_menu li:hover > ul {
+ display: block;
+ }
+}
+@media screen and (max-width: 699px) {
+ body {
+ margin: 5px 10px;
+ background: url('../img/background_mobile.png') no-repeat top center;
+ background-attachment: fixed;
+ font: 12pt "Philosopher", Georgia, serif;
+ }
+ img {
+ max-width: 100%;
+ height: auto;
+ }
+ img.thumbnail {
+ display: block;
+ float: left;
+ height: 70px;
+ width: 70px;
+ margin: 5px;
+ box-shadow: 2px 2px 5px #888;
+ }
+ img.posting_image,
+ img.partner {
+ float: left;
+ width: 99px;
+ height: 59px;
+ padding: 2px;
+ margin: 1em 0.5em 0 0;
+ border: 1px solid #babdb6;
+ }
+ ul.main_dropdown {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ }
+ ul.main_dropdown li {
+ padding: 0;
+ }
+ ul.main_dropdown a {
+ padding-left: 2em;
+ font-size: 12pt;
+ }
+ #display .grid_10 {
+ margin: 0;
+ position: relative;
+ z-index: 1;
+ }
+ #display .next,
+ #display .previous {
+ display: block;
+ position: absolute;
+ top: 0px;
+ width: 45px;
+ height: 100%;
+ margin: 0px;
+ padding: 0;
+ text-indent: 9999px;
+ overflow: hidden;
+ opacity: .5;
+ }
+ #display .next:hover,
+ #display .previous:hover {
+ opacity: .9;
+ transition: all 0.2s ease-out;
+ }
+ #display .next {
+ background: transparent url(../img/right-arrow.png) no-repeat center center;
+ right: 0px;
+ z-index: 3;
+ }
+ #display .previous {
+ background: transparent url(../img/left-arrow.png) no-repeat center center;
+ left: 0px;
+ z-index: 2;
+ }
+ #footer {
+ border-top: 1px solid black;
+ text-align: center;
+ }
+ #jumbotron {
+ background: none !important;
+ }
+ #maincontent {
+ width: 100%;
+ }
+ #mainnav {
+ display: block;
+ float: right;
+ }
+ #navigation {
+ margin: 10px 0;
+ padding: 0;
+ background: #45484d url("../img/navigation-mobile.png") top left repeat-x;
+ background-size: contain;
+ }
+ #navigation a {
+ font: bold 12px Arial;
+ color: #FFF;
+ text-decoration: none;
+ }
+ #navigation li {
+ display: inline-block;
+ padding: 0.5em 0.3em 0.5em 0.5em;
+ text-align: center;
+ border-left: 1px solid #ffffff;
+ margin: 0;
+ }
+ #navigation li:first-of-type {
+ border: none;
+ }
+ #sitelogo {
+ background: url('../img/logo_mobile.png') no-repeat;
+ width: 114px;
+ height: 54px;
+ left: 5px;
+ margin: 0;
+ padding: 0;
+ text-indent: -9999px;
+ top: 5px;
+ z-index: 20;
+ float: left;
+ }
+ #sitelogo a {
+ display: block;
+ width: 114px;
+ height: 54px;
+ }
+ #siteheader:after {
+ content: "";
+ clear: both;
+ display: block;
+ visibility: hidden;
+ height: 0px;
+ }
+ #redbox {
+ margin-top: 1em;
+ display: block;
+ }
+ #redbox h3 {
+ margin: 0.5em 0;
+ }
+ #redbox ul li {
+ margin-bottom: 0.75em;
+ }
+ #teaser {
+ background: none;
+ margin-bottom: 1em;
+ }
+ #teaser_text {
+ background: rgba(255, 255, 255, 0.5);
+ }
+ #topnav a {
+ display: inline-block;
+ color: #000;
+ font: 400 small-caps 24pt 'Amerika Sans', sans-serif;
+ min-width: 80px;
+ text-align: center;
+ text-decoration: none;
+ text-shadow: 2px 2px 2px #2e3436;
+ padding: 5px;
+ }
+ #topnav a.active {
+ color: #bc0a19;
+ }
+ #topnav a:hover {
+ color: #FFF;
+ }
+ #toggle,
+ .toggle {
+ display: none;
+ }
+ #toggle:checked ~ .main_menu {
+ display: block;
+ opacity: 1;
+ }
+ #toggle:checked ~ .toggle,
+ .toggle:hover {
+ background: #45ABD6;
+ }
+ #usernav a {
+ display: inline-block;
+ margin: 0 0.5em;
+ }
+ .comment_picture {
+ display: table-cell;
+ padding: 0px 10px;
+ width: 60px;
+ vertical-align: top;
+ }
+ .comment_header {
+ display: table-cell;
+ padding: 0px 10px;
+ width: 140px;
+ vertical-align: top;
+ }
+ .comment_header h3 {
+ margin: 0;
+ }
+ .comment {
+ display: block;
+ }
+ .comment_picture {
+ display: block;
+ float: left;
+ vertical-align: top;
+ width: 60px;
+ }
+ .comment_header {
+ display: block;
+ float: left;
+ padding: 0px 10px;
+ vertical-align: top;
+ width: 140px;
+ }
+ .comment_header h3 {
+ margin: 0;
+ }
+ .comment_text {
+ border-top: 1px solid #45484d;
+ display: block;
+ margin: 0px 10px;
+ padding-top: 0.5em;
+ clear: both;
+ }
+ .gallery {
+ float: left;
+ width: 150px;
+ height: 150px;
+ margin: 10px;
+ }
+ .gallery h3 {
+ font-size: 12pt;
+ }
+ .game img {
+ float: right;
+ margin: 0.5em 0 0.5em 1em;
+ width: 140px;
+ height: auto;
+ box-shadow: 1px 1px 5px 1px #444;
+ }
+ .game:nth-child(2n+1) img {
+ float: left;
+ margin-right: 1em;
+ margin: 0.5em 2em 0.5em 0;
+ }
+ .grid_2 {
+ min-width: 140px;
+ width: 31%;
+ margin: 1% 0 1% 0;
+ }
+ .grid_3 {
+ width: 48%;
+ margin: 1% 0 1% 0;
+ }
+ .grid_4,
+ .grid_5,
+ .grid_6,
+ .grid_7,
+ .grid_8,
+ .grid_9,
+ .grid_10,
+ .grid_11,
+ .grid_12 {
+ clear: both;
+ margin: 0;
+ width: 100%;
+ }
+ .main_menu {
+ position: absolute;
+ display: none;
+ right: 0;
+ min-width: 50%;
+ z-index: 999;
+ background: #eeeeec;
+ margin: 0;
+ padding: 0;
+ border-top: 1px solid #a40000;
+ border-bottom: 3px solid #a40000;
+ }
+ .main_menu li {
+ display: block;
+ list-style: none;
+ margin: 0;
+ }
+ .main_menu li a {
+ display: block;
+ width: 100%;
+ text-decoration: none;
+ font: 400 small-caps 18px 'Amerika Sans', sans-serif;
+ color: black;
+ box-sizing: border-box;
+ border-left: 0px solid #eeeeec;
+ padding: 0.5em 1em;
+ line-height: 1;
+ transition: all 0.25s linear;
+ }
+ .main_menu li a:hover,
+ .main_menu li a:focus {
+ color: #a40000;
+ border-left: 3px solid #a40000;
+ }
+ .officer {
+ float: left;
+ width: 25%;
+ padding: 5px;
+ box-sizing: border-box;
+ text-align: center;
+ }
+ .officer img {
+ border: 0;
+ border-radius: 50%;
+ box-shadow: 1px 1px 5px 1px #444;
+ width: 100%;
+ height: 100%;
+ }
+ .officer .function {
+ font-size: small;
+ margin-top: 0.25em;
+ }
+ .toggle {
+ background: #a40000;
+ border-radius: 5px;
+ color: #FFFFFF;
+ cursor: pointer;
+ display: block;
+ margin: 8px 0;
+ padding: 10px;
+ position: relative;
+ transition: all 0.5s linear;
+ /*user-select: none;*/
+ z-index: 2;
+ }
+ .thumbnail {
+ display: block;
+ position: relative;
+ float: left;
+ height: 70px;
+ padding: 0;
+ width: 70px;
+ margin: 5px;
+ box-shadow: 2px 2px 5px #888;
+ }
+ .thumbnail img {
+ height: 70px;
+ width: 70px;
+ }
+}
+@media screen and (min-width: 700px) {
+ body {
+ position: relative;
+ margin: 0;
+ padding: 0;
+ min-width: 960px;
+ height: 100%;
+ }
+ #body {
+ background-color: #ffffff;
+ background-image: url('../img/kranich.png'), url('../img/header_bg.jpg');
+ background-repeat: no-repeat, no-repeat;
+ background-position: center bottom, center top;
+ background-attachment: scroll, fixed;
+ }
+ #bottom_buttonbar {
+ position: absolute;
+ bottom: 0px;
+ margin: 0;
+ width: 100%;
+ }
+ #content {
+ width: 940px;
+ margin: 0px 10px;
+ position: relative;
+ }
+ #display .next,
+ #display .previous {
+ display: block;
+ position: absolute;
+ top: 0px;
+ width: 60px;
+ height: 100%;
+ margin: 0px;
+ padding: 0;
+ text-indent: 9999px;
+ overflow: hidden;
+ opacity: .5;
+ }
+ #display .next:hover,
+ #display .previous:hover {
+ opacity: 1;
+ transition: all 0.2s ease-out;
+ }
+ #display .next {
+ background: transparent url(../img/right-arrow.png) no-repeat center center;
+ right: 10px;
+ z-index: 3;
+ }
+ #display .previous {
+ background: transparent url(../img/left-arrow.png) no-repeat center center;
+ left: 10px;
+ z-index: 2;
+ }
+ #display img {
+ box-shadow: 1px 1px 5px 1px #444;
+ }
+ #footer {
+ width: 920px;
+ min-height: 50px;
+ margin: 20px auto 0 auto;
+ z-index: 30;
+ }
+ #footer p {
+ text-align: center;
+ }
+ #google_maps {
+ position: relative;
+ top: 0px;
+ left: 0px;
+ height: 280px;
+ padding: 10px;
+ border-radius: 0px 10px 10px 0px;
+ }
+ #jumbotron {
+ clear: both;
+ position: relative;
+ padding: 0;
+ width: 940px;
+ margin: 0 10px 1em 10px;
+ z-index: 5;
+ min-height: 300px;
+ border: none;
+ border-radius: 10px;
+ background-repeat: no-repeat;
+ background-color: #333;
+ background-position: center left;
+ }
+ #jumbotron > h2,
+ #jumbotron > h1 {
+ color: #eff0ef;
+ text-shadow: 1px 1px 1px #000;
+ position: absolute;
+ top: 33%;
+ left: 10px;
+ max-width: 600px;
+ margin: 0;
+ }
+ #jumbotron #teaser_text {
+ display: block;
+ position: absolute;
+ left: 0px;
+ bottom: 0px;
+ width: 620px;
+ color: #FFF;
+ background: rgba(0, 0, 0, 0.5);
+ padding: 1em;
+ border-radius: 0px 0px 0px 10px;
+ }
+ #jumbotron #teaser_text a:link,
+ #jumbotron #teaser_text a:active,
+ #jumbotron #teaser_text a:visited {
+ color: #fff;
+ text-decoration: underline;
+ }
+ #maincontent {
+ margin: 0 auto;
+ height: auto !important;
+ width: 960px;
+ min-height: 800px;
+ padding: 10px 0 2em 0;
+ position: relative;
+ z-index: 19;
+ border-radius: 10px;
+ background: rgba(255, 255, 255, 0.5);
+ box-shadow: 0px 0px 20px 1px rgba(0, 0, 0, 0.75);
+ }
+ #messages {
+ clear: both;
+ margin: 0 auto;
+ padding: 8px 0 0 30px;
+ width: 920px;
+ list-style: none;
+ }
+ #messages li.success {
+ color: #253324;
+ background: #89bd84;
+ border: 1px solid #253324;
+ border-radius: 10px;
+ margin: 10px;
+ padding: 10px;
+ }
+ #navigation {
+ clear: both;
+ background: url(../img/navigation-bg.png) no-repeat left top;
+ height: 56px;
+ margin: 0 auto;
+ padding: 8px 35px 0px 25px;
+ position: relative;
+ width: 900px;
+ z-index: 30;
+ }
+ #navigation a {
+ background: url(../img/navigation-separator.png) no-repeat right center;
+ color: #FFF;
+ display: inline-block;
+ line-height: 50px;
+ font-weight: bold;
+ height: 50px;
+ padding: 0 15px;
+ text-decoration: none;
+ }
+ #navigation a:hover,
+ #navigation a.active {
+ background: url(../img/navigation-hover.png) repeat-x left top;
+ color: #3B3B3B;
+ }
+ #navigation li {
+ display: inline;
+ }
+ #recaptcha_widget_div {
+ margin-top: -20px;
+ }
+ #redbox {
+ position: absolute;
+ top: 0px;
+ right: 0px;
+ height: 280px;
+ width: 280px;
+ border-radius: 0px 10px 10px 0px;
+ z-index: 100;
+ }
+ #redbox h3 {
+ margin: 0.5em 0;
+ }
+ #redbox ul li {
+ line-height: 1em;
+ margin-bottom: 0.5em;
+ }
+ #usernav {
+ position: absolute;
+ top: 0;
+ right: 0;
+ text-align: right;
+ background: black;
+ background: linear-gradient(to bottom, #45484d 0%, #000000 100%);
+ border-radius: 0 0 0 10px;
+ font-size: 14pt;
+ color: #FFF;
+ padding: 10px 10px;
+ z-index: 50;
+ box-shadow: -1px -1px 5px 1px rgba(0, 0, 0, 0.75);
+ }
+ #usernav a {
+ color: #FFF;
+ }
+ .comment_picture {
+ display: table-cell;
+ padding: 0px 10px;
+ width: 60px;
+ vertical-align: top;
+ }
+ .comment_header {
+ display: table-cell;
+ padding: 0px 10px;
+ width: 140px;
+ vertical-align: top;
+ }
+ .comment_header h3 {
+ margin: 0;
+ }
+ .comment_text {
+ display: table-cell;
+ padding: 0px 10px;
+ width: auto;
+ max-width: 700px;
+ }
+ .gallery {
+ display: inline;
+ float: left;
+ height: 200px;
+ margin: 10px;
+ overflow: hidden;
+ text-align: center;
+ width: 300px;
+ }
+ .gallery .thumbnail {
+ display: block;
+ float: none;
+ margin: 5px auto;
+ }
+ .game img {
+ float: right;
+ margin: 0.5em 0 0.5em 1em;
+ width: 300px;
+ height: auto;
+ box-shadow: 1px 1px 5px 1px #444;
+ }
+ .game:nth-child(2n+1) img {
+ float: left;
+ margin-right: 1em;
+ margin: 0.5em 2em 0.5em 0;
+ }
+ .officer {
+ float: left;
+ width: 140px;
+ margin: 5px;
+ box-sizing: border-box;
+ text-align: center;
+ }
+ .officer img {
+ border: 0;
+ border-radius: 50%;
+ box-shadow: 1px 1px 5px 1px #444;
+ width: 130px;
+ height: 130px;
+ }
+ .officer .function {
+ font-size: small;
+ margin-top: 0.25em;
+ }
+ img.posting_image,
+ img.partner {
+ float: left;
+ width: 200px;
+ height: 120px;
+ padding: 2px;
+ margin: 0 1em 1em 0;
+ border: 1px solid #babdb6;
+ }
+ .thumbnail a.delete_image {
+ position: absolute;
+ right: 4px;
+ bottom: 0px;
+ }
+}
+@page {
+ margin: 1cm 1cm 1cm 2cm;
+ size: A4 portrait;
+}
+@media print {
+ a:link,
+ a:visited {
+ color: black;
+ font-weight: bold;
+ }
+ body,
+ article {
+ width: 100%;
+ margin: 0;
+ padding: 0;
+ color: #000;
+ background: #fff;
+ }
+ h1 {
+ font-size: 32pt;
+ }
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ text-shadow: none;
+ page-break-after: avoid;
+ }
+ img {
+ max-width: 100% !important;
+ page-break-inside: avoid;
+ }
+ nav,
+ aside {
+ display: none;
+ }
+ ul {
+ page-break-inside: avoid;
+ }
+ #footer {
+ width: 100%;
+ padding-top: 0.5em;
+ border-top: 1px solid black;
+ text-align: center;
+ }
+ #jumbotron {
+ background: none !important;
+ }
+ #maincontent nav {
+ display: none;
+ }
+ #maincontent aside {
+ display: none;
+ }
+ #sitelogo {
+ background: url(../img/logo.png) top right no-repeat;
+ background-size: contain;
+ left: 0;
+ margin: 0;
+ padding: 0;
+ line-height: 1cm;
+ font-family: 'Amerika Sans', Helvetica;
+ font-size: 8pt;
+ top: 5px;
+ z-index: 99;
+ }
+ #comment_form,
+ #comments,
+ #footer,
+ #navigation,
+ #mainnav,
+ #usernav,
+ #bottom_buttonbar,
+ #footer > form {
+ display: none;
+ }
+ .grid_6,
+ grid_7,
+ .grid_8,
+ grid_9,
+ .grid_10,
+ .grid_11,
+ .grid_12 {
+ width: 100%;
+ }
+ .more_link {
+ display: none;
+ }
+}
+.fa {
+ display: inline-block;
+ font: normal normal normal 14px/1 FontAwesome;
+ font-size: inherit;
+ text-rendering: auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+/* makes the font 33% larger relative to the icon container */
+.fa-lg {
+ font-size: 1.33333333em;
+ line-height: 0.75em;
+ vertical-align: -15%;
+}
+.fa-2x {
+ font-size: 2em;
+}
+.fa-3x {
+ font-size: 3em;
+}
+.fa-4x {
+ font-size: 4em;
+}
+.fa-5x {
+ font-size: 5em;
+}
+.fa-fw {
+ width: 1.28571429em;
+ text-align: center;
+}
+.fa-ul {
+ padding-left: 0;
+ margin-left: 2.14285714em;
+ list-style-type: none;
+}
+.fa-ul > li {
+ position: relative;
+}
+.fa-li {
+ position: absolute;
+ left: -2.14285714em;
+ width: 2.14285714em;
+ top: 0.14285714em;
+ text-align: center;
+}
+.fa-li.fa-lg {
+ left: -1.85714286em;
+}
+/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen
+ readers do not read off random characters that represent icons */
+.fa-glass:before {
+ content: "\f000";
+}
+.fa-music:before {
+ content: "\f001";
+}
+.fa-search:before {
+ content: "\f002";
+}
+.fa-envelope-o:before {
+ content: "\f003";
+}
+.fa-heart:before {
+ content: "\f004";
+}
+.fa-star:before {
+ content: "\f005";
+}
+.fa-star-o:before {
+ content: "\f006";
+}
+.fa-user:before {
+ content: "\f007";
+}
+.fa-film:before {
+ content: "\f008";
+}
+.fa-th-large:before {
+ content: "\f009";
+}
+.fa-th:before {
+ content: "\f00a";
+}
+.fa-th-list:before {
+ content: "\f00b";
+}
+.fa-check:before {
+ content: "\f00c";
+}
+.fa-remove:before,
+.fa-close:before,
+.fa-times:before {
+ content: "\f00d";
+}
+.fa-search-plus:before {
+ content: "\f00e";
+}
+.fa-search-minus:before {
+ content: "\f010";
+}
+.fa-power-off:before {
+ content: "\f011";
+}
+.fa-signal:before {
+ content: "\f012";
+}
+.fa-gear:before,
+.fa-cog:before {
+ content: "\f013";
+}
+.fa-trash-o:before {
+ content: "\f014";
+}
+.fa-home:before {
+ content: "\f015";
+}
+.fa-file-o:before {
+ content: "\f016";
+}
+.fa-clock-o:before {
+ content: "\f017";
+}
+.fa-road:before {
+ content: "\f018";
+}
+.fa-download:before {
+ content: "\f019";
+}
+.fa-arrow-circle-o-down:before {
+ content: "\f01a";
+}
+.fa-arrow-circle-o-up:before {
+ content: "\f01b";
+}
+.fa-inbox:before {
+ content: "\f01c";
+}
+.fa-play-circle-o:before {
+ content: "\f01d";
+}
+.fa-rotate-right:before,
+.fa-repeat:before {
+ content: "\f01e";
+}
+.fa-refresh:before {
+ content: "\f021";
+}
+.fa-list-alt:before {
+ content: "\f022";
+}
+.fa-lock:before {
+ content: "\f023";
+}
+.fa-flag:before {
+ content: "\f024";
+}
+.fa-headphones:before {
+ content: "\f025";
+}
+.fa-volume-off:before {
+ content: "\f026";
+}
+.fa-volume-down:before {
+ content: "\f027";
+}
+.fa-volume-up:before {
+ content: "\f028";
+}
+.fa-qrcode:before {
+ content: "\f029";
+}
+.fa-barcode:before {
+ content: "\f02a";
+}
+.fa-tag:before {
+ content: "\f02b";
+}
+.fa-tags:before {
+ content: "\f02c";
+}
+.fa-book:before {
+ content: "\f02d";
+}
+.fa-bookmark:before {
+ content: "\f02e";
+}
+.fa-print:before {
+ content: "\f02f";
+}
+.fa-camera:before {
+ content: "\f030";
+}
+.fa-font:before {
+ content: "\f031";
+}
+.fa-bold:before {
+ content: "\f032";
+}
+.fa-italic:before {
+ content: "\f033";
+}
+.fa-text-height:before {
+ content: "\f034";
+}
+.fa-text-width:before {
+ content: "\f035";
+}
+.fa-align-left:before {
+ content: "\f036";
+}
+.fa-align-center:before {
+ content: "\f037";
+}
+.fa-align-right:before {
+ content: "\f038";
+}
+.fa-align-justify:before {
+ content: "\f039";
+}
+.fa-list:before {
+ content: "\f03a";
+}
+.fa-dedent:before,
+.fa-outdent:before {
+ content: "\f03b";
+}
+.fa-indent:before {
+ content: "\f03c";
+}
+.fa-video-camera:before {
+ content: "\f03d";
+}
+.fa-photo:before,
+.fa-image:before,
+.fa-picture-o:before {
+ content: "\f03e";
+}
+.fa-pencil:before {
+ content: "\f040";
+}
+.fa-map-marker:before {
+ content: "\f041";
+}
+.fa-adjust:before {
+ content: "\f042";
+}
+.fa-tint:before {
+ content: "\f043";
+}
+.fa-edit:before,
+.fa-pencil-square-o:before {
+ content: "\f044";
+}
+.fa-share-square-o:before {
+ content: "\f045";
+}
+.fa-check-square-o:before {
+ content: "\f046";
+}
+.fa-arrows:before {
+ content: "\f047";
+}
+.fa-step-backward:before {
+ content: "\f048";
+}
+.fa-fast-backward:before {
+ content: "\f049";
+}
+.fa-backward:before {
+ content: "\f04a";
+}
+.fa-play:before {
+ content: "\f04b";
+}
+.fa-pause:before {
+ content: "\f04c";
+}
+.fa-stop:before {
+ content: "\f04d";
+}
+.fa-forward:before {
+ content: "\f04e";
+}
+.fa-fast-forward:before {
+ content: "\f050";
+}
+.fa-step-forward:before {
+ content: "\f051";
+}
+.fa-eject:before {
+ content: "\f052";
+}
+.fa-chevron-left:before {
+ content: "\f053";
+}
+.fa-chevron-right:before {
+ content: "\f054";
+}
+.fa-plus-circle:before {
+ content: "\f055";
+}
+.fa-minus-circle:before {
+ content: "\f056";
+}
+.fa-times-circle:before {
+ content: "\f057";
+}
+.fa-check-circle:before {
+ content: "\f058";
+}
+.fa-question-circle:before {
+ content: "\f059";
+}
+.fa-info-circle:before {
+ content: "\f05a";
+}
+.fa-crosshairs:before {
+ content: "\f05b";
+}
+.fa-times-circle-o:before {
+ content: "\f05c";
+}
+.fa-check-circle-o:before {
+ content: "\f05d";
+}
+.fa-ban:before {
+ content: "\f05e";
+}
+.fa-arrow-left:before {
+ content: "\f060";
+}
+.fa-arrow-right:before {
+ content: "\f061";
+}
+.fa-arrow-up:before {
+ content: "\f062";
+}
+.fa-arrow-down:before {
+ content: "\f063";
+}
+.fa-mail-forward:before,
+.fa-share:before {
+ content: "\f064";
+}
+.fa-expand:before {
+ content: "\f065";
+}
+.fa-compress:before {
+ content: "\f066";
+}
+.fa-plus:before {
+ content: "\f067";
+}
+.fa-minus:before {
+ content: "\f068";
+}
+.fa-asterisk:before {
+ content: "\f069";
+}
+.fa-exclamation-circle:before {
+ content: "\f06a";
+}
+.fa-gift:before {
+ content: "\f06b";
+}
+.fa-leaf:before {
+ content: "\f06c";
+}
+.fa-fire:before {
+ content: "\f06d";
+}
+.fa-eye:before {
+ content: "\f06e";
+}
+.fa-eye-slash:before {
+ content: "\f070";
+}
+.fa-warning:before,
+.fa-exclamation-triangle:before {
+ content: "\f071";
+}
+.fa-plane:before {
+ content: "\f072";
+}
+.fa-calendar:before {
+ content: "\f073";
+}
+.fa-random:before {
+ content: "\f074";
+}
+.fa-comment:before {
+ content: "\f075";
+}
+.fa-magnet:before {
+ content: "\f076";
+}
+.fa-chevron-up:before {
+ content: "\f077";
+}
+.fa-chevron-down:before {
+ content: "\f078";
+}
+.fa-retweet:before {
+ content: "\f079";
+}
+.fa-shopping-cart:before {
+ content: "\f07a";
+}
+.fa-folder:before {
+ content: "\f07b";
+}
+.fa-folder-open:before {
+ content: "\f07c";
+}
+.fa-arrows-v:before {
+ content: "\f07d";
+}
+.fa-arrows-h:before {
+ content: "\f07e";
+}
+.fa-bar-chart-o:before,
+.fa-bar-chart:before {
+ content: "\f080";
+}
+.fa-twitter-square:before {
+ content: "\f081";
+}
+.fa-facebook-square:before {
+ content: "\f082";
+}
+.fa-camera-retro:before {
+ content: "\f083";
+}
+.fa-key:before {
+ content: "\f084";
+}
+.fa-gears:before,
+.fa-cogs:before {
+ content: "\f085";
+}
+.fa-comments:before {
+ content: "\f086";
+}
+.fa-thumbs-o-up:before {
+ content: "\f087";
+}
+.fa-thumbs-o-down:before {
+ content: "\f088";
+}
+.fa-star-half:before {
+ content: "\f089";
+}
+.fa-heart-o:before {
+ content: "\f08a";
+}
+.fa-sign-out:before {
+ content: "\f08b";
+}
+.fa-linkedin-square:before {
+ content: "\f08c";
+}
+.fa-thumb-tack:before {
+ content: "\f08d";
+}
+.fa-external-link:before {
+ content: "\f08e";
+}
+.fa-sign-in:before {
+ content: "\f090";
+}
+.fa-trophy:before {
+ content: "\f091";
+}
+.fa-github-square:before {
+ content: "\f092";
+}
+.fa-upload:before {
+ content: "\f093";
+}
+.fa-lemon-o:before {
+ content: "\f094";
+}
+.fa-phone:before {
+ content: "\f095";
+}
+.fa-square-o:before {
+ content: "\f096";
+}
+.fa-bookmark-o:before {
+ content: "\f097";
+}
+.fa-phone-square:before {
+ content: "\f098";
+}
+.fa-twitter:before {
+ content: "\f099";
+}
+.fa-facebook-f:before,
+.fa-facebook:before {
+ content: "\f09a";
+}
+.fa-github:before {
+ content: "\f09b";
+}
+.fa-unlock:before {
+ content: "\f09c";
+}
+.fa-credit-card:before {
+ content: "\f09d";
+}
+.fa-feed:before,
+.fa-rss:before {
+ content: "\f09e";
+}
+.fa-hdd-o:before {
+ content: "\f0a0";
+}
+.fa-bullhorn:before {
+ content: "\f0a1";
+}
+.fa-bell:before {
+ content: "\f0f3";
+}
+.fa-certificate:before {
+ content: "\f0a3";
+}
+.fa-hand-o-right:before {
+ content: "\f0a4";
+}
+.fa-hand-o-left:before {
+ content: "\f0a5";
+}
+.fa-hand-o-up:before {
+ content: "\f0a6";
+}
+.fa-hand-o-down:before {
+ content: "\f0a7";
+}
+.fa-arrow-circle-left:before {
+ content: "\f0a8";
+}
+.fa-arrow-circle-right:before {
+ content: "\f0a9";
+}
+.fa-arrow-circle-up:before {
+ content: "\f0aa";
+}
+.fa-arrow-circle-down:before {
+ content: "\f0ab";
+}
+.fa-globe:before {
+ content: "\f0ac";
+}
+.fa-wrench:before {
+ content: "\f0ad";
+}
+.fa-tasks:before {
+ content: "\f0ae";
+}
+.fa-filter:before {
+ content: "\f0b0";
+}
+.fa-briefcase:before {
+ content: "\f0b1";
+}
+.fa-arrows-alt:before {
+ content: "\f0b2";
+}
+.fa-group:before,
+.fa-users:before {
+ content: "\f0c0";
+}
+.fa-chain:before,
+.fa-link:before {
+ content: "\f0c1";
+}
+.fa-cloud:before {
+ content: "\f0c2";
+}
+.fa-flask:before {
+ content: "\f0c3";
+}
+.fa-cut:before,
+.fa-scissors:before {
+ content: "\f0c4";
+}
+.fa-copy:before,
+.fa-files-o:before {
+ content: "\f0c5";
+}
+.fa-paperclip:before {
+ content: "\f0c6";
+}
+.fa-save:before,
+.fa-floppy-o:before {
+ content: "\f0c7";
+}
+.fa-square:before {
+ content: "\f0c8";
+}
+.fa-navicon:before,
+.fa-reorder:before,
+.fa-bars:before {
+ content: "\f0c9";
+}
+.fa-list-ul:before {
+ content: "\f0ca";
+}
+.fa-list-ol:before {
+ content: "\f0cb";
+}
+.fa-strikethrough:before {
+ content: "\f0cc";
+}
+.fa-underline:before {
+ content: "\f0cd";
+}
+.fa-table:before {
+ content: "\f0ce";
+}
+.fa-magic:before {
+ content: "\f0d0";
+}
+.fa-truck:before {
+ content: "\f0d1";
+}
+.fa-pinterest:before {
+ content: "\f0d2";
+}
+.fa-pinterest-square:before {
+ content: "\f0d3";
+}
+.fa-google-plus-square:before {
+ content: "\f0d4";
+}
+.fa-google-plus:before {
+ content: "\f0d5";
+}
+.fa-money:before {
+ content: "\f0d6";
+}
+.fa-caret-down:before {
+ content: "\f0d7";
+}
+.fa-caret-up:before {
+ content: "\f0d8";
+}
+.fa-caret-left:before {
+ content: "\f0d9";
+}
+.fa-caret-right:before {
+ content: "\f0da";
+}
+.fa-columns:before {
+ content: "\f0db";
+}
+.fa-unsorted:before,
+.fa-sort:before {
+ content: "\f0dc";
+}
+.fa-sort-down:before,
+.fa-sort-desc:before {
+ content: "\f0dd";
+}
+.fa-sort-up:before,
+.fa-sort-asc:before {
+ content: "\f0de";
+}
+.fa-envelope:before {
+ content: "\f0e0";
+}
+.fa-linkedin:before {
+ content: "\f0e1";
+}
+.fa-rotate-left:before,
+.fa-undo:before {
+ content: "\f0e2";
+}
+.fa-legal:before,
+.fa-gavel:before {
+ content: "\f0e3";
+}
+.fa-dashboard:before,
+.fa-tachometer:before {
+ content: "\f0e4";
+}
+.fa-comment-o:before {
+ content: "\f0e5";
+}
+.fa-comments-o:before {
+ content: "\f0e6";
+}
+.fa-flash:before,
+.fa-bolt:before {
+ content: "\f0e7";
+}
+.fa-sitemap:before {
+ content: "\f0e8";
+}
+.fa-umbrella:before {
+ content: "\f0e9";
+}
+.fa-paste:before,
+.fa-clipboard:before {
+ content: "\f0ea";
+}
+.fa-lightbulb-o:before {
+ content: "\f0eb";
+}
+.fa-exchange:before {
+ content: "\f0ec";
+}
+.fa-cloud-download:before {
+ content: "\f0ed";
+}
+.fa-cloud-upload:before {
+ content: "\f0ee";
+}
+.fa-user-md:before {
+ content: "\f0f0";
+}
+.fa-stethoscope:before {
+ content: "\f0f1";
+}
+.fa-suitcase:before {
+ content: "\f0f2";
+}
+.fa-bell-o:before {
+ content: "\f0a2";
+}
+.fa-coffee:before {
+ content: "\f0f4";
+}
+.fa-cutlery:before {
+ content: "\f0f5";
+}
+.fa-file-text-o:before {
+ content: "\f0f6";
+}
+.fa-building-o:before {
+ content: "\f0f7";
+}
+.fa-hospital-o:before {
+ content: "\f0f8";
+}
+.fa-ambulance:before {
+ content: "\f0f9";
+}
+.fa-medkit:before {
+ content: "\f0fa";
+}
+.fa-fighter-jet:before {
+ content: "\f0fb";
+}
+.fa-beer:before {
+ content: "\f0fc";
+}
+.fa-h-square:before {
+ content: "\f0fd";
+}
+.fa-plus-square:before {
+ content: "\f0fe";
+}
+.fa-angle-double-left:before {
+ content: "\f100";
+}
+.fa-angle-double-right:before {
+ content: "\f101";
+}
+.fa-angle-double-up:before {
+ content: "\f102";
+}
+.fa-angle-double-down:before {
+ content: "\f103";
+}
+.fa-angle-left:before {
+ content: "\f104";
+}
+.fa-angle-right:before {
+ content: "\f105";
+}
+.fa-angle-up:before {
+ content: "\f106";
+}
+.fa-angle-down:before {
+ content: "\f107";
+}
+.fa-desktop:before {
+ content: "\f108";
+}
+.fa-laptop:before {
+ content: "\f109";
+}
+.fa-tablet:before {
+ content: "\f10a";
+}
+.fa-mobile-phone:before,
+.fa-mobile:before {
+ content: "\f10b";
+}
+.fa-circle-o:before {
+ content: "\f10c";
+}
+.fa-quote-left:before {
+ content: "\f10d";
+}
+.fa-quote-right:before {
+ content: "\f10e";
+}
+.fa-spinner:before {
+ content: "\f110";
+}
+.fa-circle:before {
+ content: "\f111";
+}
+.fa-mail-reply:before,
+.fa-reply:before {
+ content: "\f112";
+}
+.fa-github-alt:before {
+ content: "\f113";
+}
+.fa-folder-o:before {
+ content: "\f114";
+}
+.fa-folder-open-o:before {
+ content: "\f115";
+}
+.fa-smile-o:before {
+ content: "\f118";
+}
+.fa-frown-o:before {
+ content: "\f119";
+}
+.fa-meh-o:before {
+ content: "\f11a";
+}
+.fa-gamepad:before {
+ content: "\f11b";
+}
+.fa-keyboard-o:before {
+ content: "\f11c";
+}
+.fa-flag-o:before {
+ content: "\f11d";
+}
+.fa-flag-checkered:before {
+ content: "\f11e";
+}
+.fa-terminal:before {
+ content: "\f120";
+}
+.fa-code:before {
+ content: "\f121";
+}
+.fa-mail-reply-all:before,
+.fa-reply-all:before {
+ content: "\f122";
+}
+.fa-star-half-empty:before,
+.fa-star-half-full:before,
+.fa-star-half-o:before {
+ content: "\f123";
+}
+.fa-location-arrow:before {
+ content: "\f124";
+}
+.fa-crop:before {
+ content: "\f125";
+}
+.fa-code-fork:before {
+ content: "\f126";
+}
+.fa-unlink:before,
+.fa-chain-broken:before {
+ content: "\f127";
+}
+.fa-question:before {
+ content: "\f128";
+}
+.fa-info:before {
+ content: "\f129";
+}
+.fa-exclamation:before {
+ content: "\f12a";
+}
+.fa-superscript:before {
+ content: "\f12b";
+}
+.fa-subscript:before {
+ content: "\f12c";
+}
+.fa-eraser:before {
+ content: "\f12d";
+}
+.fa-puzzle-piece:before {
+ content: "\f12e";
+}
+.fa-microphone:before {
+ content: "\f130";
+}
+.fa-microphone-slash:before {
+ content: "\f131";
+}
+.fa-shield:before {
+ content: "\f132";
+}
+.fa-calendar-o:before {
+ content: "\f133";
+}
+.fa-fire-extinguisher:before {
+ content: "\f134";
+}
+.fa-rocket:before {
+ content: "\f135";
+}
+.fa-maxcdn:before {
+ content: "\f136";
+}
+.fa-chevron-circle-left:before {
+ content: "\f137";
+}
+.fa-chevron-circle-right:before {
+ content: "\f138";
+}
+.fa-chevron-circle-up:before {
+ content: "\f139";
+}
+.fa-chevron-circle-down:before {
+ content: "\f13a";
+}
+.fa-html5:before {
+ content: "\f13b";
+}
+.fa-css3:before {
+ content: "\f13c";
+}
+.fa-anchor:before {
+ content: "\f13d";
+}
+.fa-unlock-alt:before {
+ content: "\f13e";
+}
+.fa-bullseye:before {
+ content: "\f140";
+}
+.fa-ellipsis-h:before {
+ content: "\f141";
+}
+.fa-ellipsis-v:before {
+ content: "\f142";
+}
+.fa-rss-square:before {
+ content: "\f143";
+}
+.fa-play-circle:before {
+ content: "\f144";
+}
+.fa-ticket:before {
+ content: "\f145";
+}
+.fa-minus-square:before {
+ content: "\f146";
+}
+.fa-minus-square-o:before {
+ content: "\f147";
+}
+.fa-level-up:before {
+ content: "\f148";
+}
+.fa-level-down:before {
+ content: "\f149";
+}
+.fa-check-square:before {
+ content: "\f14a";
+}
+.fa-pencil-square:before {
+ content: "\f14b";
+}
+.fa-external-link-square:before {
+ content: "\f14c";
+}
+.fa-share-square:before {
+ content: "\f14d";
+}
+.fa-compass:before {
+ content: "\f14e";
+}
+.fa-toggle-down:before,
+.fa-caret-square-o-down:before {
+ content: "\f150";
+}
+.fa-toggle-up:before,
+.fa-caret-square-o-up:before {
+ content: "\f151";
+}
+.fa-toggle-right:before,
+.fa-caret-square-o-right:before {
+ content: "\f152";
+}
+.fa-euro:before,
+.fa-eur:before {
+ content: "\f153";
+}
+.fa-gbp:before {
+ content: "\f154";
+}
+.fa-dollar:before,
+.fa-usd:before {
+ content: "\f155";
+}
+.fa-rupee:before,
+.fa-inr:before {
+ content: "\f156";
+}
+.fa-cny:before,
+.fa-rmb:before,
+.fa-yen:before,
+.fa-jpy:before {
+ content: "\f157";
+}
+.fa-ruble:before,
+.fa-rouble:before,
+.fa-rub:before {
+ content: "\f158";
+}
+.fa-won:before,
+.fa-krw:before {
+ content: "\f159";
+}
+.fa-bitcoin:before,
+.fa-btc:before {
+ content: "\f15a";
+}
+.fa-file:before {
+ content: "\f15b";
+}
+.fa-file-text:before {
+ content: "\f15c";
+}
+.fa-sort-alpha-asc:before {
+ content: "\f15d";
+}
+.fa-sort-alpha-desc:before {
+ content: "\f15e";
+}
+.fa-sort-amount-asc:before {
+ content: "\f160";
+}
+.fa-sort-amount-desc:before {
+ content: "\f161";
+}
+.fa-sort-numeric-asc:before {
+ content: "\f162";
+}
+.fa-sort-numeric-desc:before {
+ content: "\f163";
+}
+.fa-thumbs-up:before {
+ content: "\f164";
+}
+.fa-thumbs-down:before {
+ content: "\f165";
+}
+.fa-youtube-square:before {
+ content: "\f166";
+}
+.fa-youtube:before {
+ content: "\f167";
+}
+.fa-xing:before {
+ content: "\f168";
+}
+.fa-xing-square:before {
+ content: "\f169";
+}
+.fa-youtube-play:before {
+ content: "\f16a";
+}
+.fa-dropbox:before {
+ content: "\f16b";
+}
+.fa-stack-overflow:before {
+ content: "\f16c";
+}
+.fa-instagram:before {
+ content: "\f16d";
+}
+.fa-flickr:before {
+ content: "\f16e";
+}
+.fa-adn:before {
+ content: "\f170";
+}
+.fa-bitbucket:before {
+ content: "\f171";
+}
+.fa-bitbucket-square:before {
+ content: "\f172";
+}
+.fa-tumblr:before {
+ content: "\f173";
+}
+.fa-tumblr-square:before {
+ content: "\f174";
+}
+.fa-long-arrow-down:before {
+ content: "\f175";
+}
+.fa-long-arrow-up:before {
+ content: "\f176";
+}
+.fa-long-arrow-left:before {
+ content: "\f177";
+}
+.fa-long-arrow-right:before {
+ content: "\f178";
+}
+.fa-apple:before {
+ content: "\f179";
+}
+.fa-windows:before {
+ content: "\f17a";
+}
+.fa-android:before {
+ content: "\f17b";
+}
+.fa-linux:before {
+ content: "\f17c";
+}
+.fa-dribbble:before {
+ content: "\f17d";
+}
+.fa-skype:before {
+ content: "\f17e";
+}
+.fa-foursquare:before {
+ content: "\f180";
+}
+.fa-trello:before {
+ content: "\f181";
+}
+.fa-female:before {
+ content: "\f182";
+}
+.fa-male:before {
+ content: "\f183";
+}
+.fa-gittip:before,
+.fa-gratipay:before {
+ content: "\f184";
+}
+.fa-sun-o:before {
+ content: "\f185";
+}
+.fa-moon-o:before {
+ content: "\f186";
+}
+.fa-archive:before {
+ content: "\f187";
+}
+.fa-bug:before {
+ content: "\f188";
+}
+.fa-vk:before {
+ content: "\f189";
+}
+.fa-weibo:before {
+ content: "\f18a";
+}
+.fa-renren:before {
+ content: "\f18b";
+}
+.fa-pagelines:before {
+ content: "\f18c";
+}
+.fa-stack-exchange:before {
+ content: "\f18d";
+}
+.fa-arrow-circle-o-right:before {
+ content: "\f18e";
+}
+.fa-arrow-circle-o-left:before {
+ content: "\f190";
+}
+.fa-toggle-left:before,
+.fa-caret-square-o-left:before {
+ content: "\f191";
+}
+.fa-dot-circle-o:before {
+ content: "\f192";
+}
+.fa-wheelchair:before {
+ content: "\f193";
+}
+.fa-vimeo-square:before {
+ content: "\f194";
+}
+.fa-turkish-lira:before,
+.fa-try:before {
+ content: "\f195";
+}
+.fa-plus-square-o:before {
+ content: "\f196";
+}
+.fa-space-shuttle:before {
+ content: "\f197";
+}
+.fa-slack:before {
+ content: "\f198";
+}
+.fa-envelope-square:before {
+ content: "\f199";
+}
+.fa-wordpress:before {
+ content: "\f19a";
+}
+.fa-openid:before {
+ content: "\f19b";
+}
+.fa-institution:before,
+.fa-bank:before,
+.fa-university:before {
+ content: "\f19c";
+}
+.fa-mortar-board:before,
+.fa-graduation-cap:before {
+ content: "\f19d";
+}
+.fa-yahoo:before {
+ content: "\f19e";
+}
+.fa-google:before {
+ content: "\f1a0";
+}
+.fa-reddit:before {
+ content: "\f1a1";
+}
+.fa-reddit-square:before {
+ content: "\f1a2";
+}
+.fa-stumbleupon-circle:before {
+ content: "\f1a3";
+}
+.fa-stumbleupon:before {
+ content: "\f1a4";
+}
+.fa-delicious:before {
+ content: "\f1a5";
+}
+.fa-digg:before {
+ content: "\f1a6";
+}
+.fa-pied-piper:before {
+ content: "\f1a7";
+}
+.fa-pied-piper-alt:before {
+ content: "\f1a8";
+}
+.fa-drupal:before {
+ content: "\f1a9";
+}
+.fa-joomla:before {
+ content: "\f1aa";
+}
+.fa-language:before {
+ content: "\f1ab";
+}
+.fa-fax:before {
+ content: "\f1ac";
+}
+.fa-building:before {
+ content: "\f1ad";
+}
+.fa-child:before {
+ content: "\f1ae";
+}
+.fa-paw:before {
+ content: "\f1b0";
+}
+.fa-spoon:before {
+ content: "\f1b1";
+}
+.fa-cube:before {
+ content: "\f1b2";
+}
+.fa-cubes:before {
+ content: "\f1b3";
+}
+.fa-behance:before {
+ content: "\f1b4";
+}
+.fa-behance-square:before {
+ content: "\f1b5";
+}
+.fa-steam:before {
+ content: "\f1b6";
+}
+.fa-steam-square:before {
+ content: "\f1b7";
+}
+.fa-recycle:before {
+ content: "\f1b8";
+}
+.fa-automobile:before,
+.fa-car:before {
+ content: "\f1b9";
+}
+.fa-cab:before,
+.fa-taxi:before {
+ content: "\f1ba";
+}
+.fa-tree:before {
+ content: "\f1bb";
+}
+.fa-spotify:before {
+ content: "\f1bc";
+}
+.fa-deviantart:before {
+ content: "\f1bd";
+}
+.fa-soundcloud:before {
+ content: "\f1be";
+}
+.fa-database:before {
+ content: "\f1c0";
+}
+.fa-file-pdf-o:before {
+ content: "\f1c1";
+}
+.fa-file-word-o:before {
+ content: "\f1c2";
+}
+.fa-file-excel-o:before {
+ content: "\f1c3";
+}
+.fa-file-powerpoint-o:before {
+ content: "\f1c4";
+}
+.fa-file-photo-o:before,
+.fa-file-picture-o:before,
+.fa-file-image-o:before {
+ content: "\f1c5";
+}
+.fa-file-zip-o:before,
+.fa-file-archive-o:before {
+ content: "\f1c6";
+}
+.fa-file-sound-o:before,
+.fa-file-audio-o:before {
+ content: "\f1c7";
+}
+.fa-file-movie-o:before,
+.fa-file-video-o:before {
+ content: "\f1c8";
+}
+.fa-file-code-o:before {
+ content: "\f1c9";
+}
+.fa-vine:before {
+ content: "\f1ca";
+}
+.fa-codepen:before {
+ content: "\f1cb";
+}
+.fa-jsfiddle:before {
+ content: "\f1cc";
+}
+.fa-life-bouy:before,
+.fa-life-buoy:before,
+.fa-life-saver:before,
+.fa-support:before,
+.fa-life-ring:before {
+ content: "\f1cd";
+}
+.fa-circle-o-notch:before {
+ content: "\f1ce";
+}
+.fa-ra:before,
+.fa-rebel:before {
+ content: "\f1d0";
+}
+.fa-ge:before,
+.fa-empire:before {
+ content: "\f1d1";
+}
+.fa-git-square:before {
+ content: "\f1d2";
+}
+.fa-git:before {
+ content: "\f1d3";
+}
+.fa-y-combinator-square:before,
+.fa-yc-square:before,
+.fa-hacker-news:before {
+ content: "\f1d4";
+}
+.fa-tencent-weibo:before {
+ content: "\f1d5";
+}
+.fa-qq:before {
+ content: "\f1d6";
+}
+.fa-wechat:before,
+.fa-weixin:before {
+ content: "\f1d7";
+}
+.fa-send:before,
+.fa-paper-plane:before {
+ content: "\f1d8";
+}
+.fa-send-o:before,
+.fa-paper-plane-o:before {
+ content: "\f1d9";
+}
+.fa-history:before {
+ content: "\f1da";
+}
+.fa-circle-thin:before {
+ content: "\f1db";
+}
+.fa-header:before {
+ content: "\f1dc";
+}
+.fa-paragraph:before {
+ content: "\f1dd";
+}
+.fa-sliders:before {
+ content: "\f1de";
+}
+.fa-share-alt:before {
+ content: "\f1e0";
+}
+.fa-share-alt-square:before {
+ content: "\f1e1";
+}
+.fa-bomb:before {
+ content: "\f1e2";
+}
+.fa-soccer-ball-o:before,
+.fa-futbol-o:before {
+ content: "\f1e3";
+}
+.fa-tty:before {
+ content: "\f1e4";
+}
+.fa-binoculars:before {
+ content: "\f1e5";
+}
+.fa-plug:before {
+ content: "\f1e6";
+}
+.fa-slideshare:before {
+ content: "\f1e7";
+}
+.fa-twitch:before {
+ content: "\f1e8";
+}
+.fa-yelp:before {
+ content: "\f1e9";
+}
+.fa-newspaper-o:before {
+ content: "\f1ea";
+}
+.fa-wifi:before {
+ content: "\f1eb";
+}
+.fa-calculator:before {
+ content: "\f1ec";
+}
+.fa-paypal:before {
+ content: "\f1ed";
+}
+.fa-google-wallet:before {
+ content: "\f1ee";
+}
+.fa-cc-visa:before {
+ content: "\f1f0";
+}
+.fa-cc-mastercard:before {
+ content: "\f1f1";
+}
+.fa-cc-discover:before {
+ content: "\f1f2";
+}
+.fa-cc-amex:before {
+ content: "\f1f3";
+}
+.fa-cc-paypal:before {
+ content: "\f1f4";
+}
+.fa-cc-stripe:before {
+ content: "\f1f5";
+}
+.fa-bell-slash:before {
+ content: "\f1f6";
+}
+.fa-bell-slash-o:before {
+ content: "\f1f7";
+}
+.fa-trash:before {
+ content: "\f1f8";
+}
+.fa-copyright:before {
+ content: "\f1f9";
+}
+.fa-at:before {
+ content: "\f1fa";
+}
+.fa-eyedropper:before {
+ content: "\f1fb";
+}
+.fa-paint-brush:before {
+ content: "\f1fc";
+}
+.fa-birthday-cake:before {
+ content: "\f1fd";
+}
+.fa-area-chart:before {
+ content: "\f1fe";
+}
+.fa-pie-chart:before {
+ content: "\f200";
+}
+.fa-line-chart:before {
+ content: "\f201";
+}
+.fa-lastfm:before {
+ content: "\f202";
+}
+.fa-lastfm-square:before {
+ content: "\f203";
+}
+.fa-toggle-off:before {
+ content: "\f204";
+}
+.fa-toggle-on:before {
+ content: "\f205";
+}
+.fa-bicycle:before {
+ content: "\f206";
+}
+.fa-bus:before {
+ content: "\f207";
+}
+.fa-ioxhost:before {
+ content: "\f208";
+}
+.fa-angellist:before {
+ content: "\f209";
+}
+.fa-cc:before {
+ content: "\f20a";
+}
+.fa-shekel:before,
+.fa-sheqel:before,
+.fa-ils:before {
+ content: "\f20b";
+}
+.fa-meanpath:before {
+ content: "\f20c";
+}
+.fa-buysellads:before {
+ content: "\f20d";
+}
+.fa-connectdevelop:before {
+ content: "\f20e";
+}
+.fa-dashcube:before {
+ content: "\f210";
+}
+.fa-forumbee:before {
+ content: "\f211";
+}
+.fa-leanpub:before {
+ content: "\f212";
+}
+.fa-sellsy:before {
+ content: "\f213";
+}
+.fa-shirtsinbulk:before {
+ content: "\f214";
+}
+.fa-simplybuilt:before {
+ content: "\f215";
+}
+.fa-skyatlas:before {
+ content: "\f216";
+}
+.fa-cart-plus:before {
+ content: "\f217";
+}
+.fa-cart-arrow-down:before {
+ content: "\f218";
+}
+.fa-diamond:before {
+ content: "\f219";
+}
+.fa-ship:before {
+ content: "\f21a";
+}
+.fa-user-secret:before {
+ content: "\f21b";
+}
+.fa-motorcycle:before {
+ content: "\f21c";
+}
+.fa-street-view:before {
+ content: "\f21d";
+}
+.fa-heartbeat:before {
+ content: "\f21e";
+}
+.fa-venus:before {
+ content: "\f221";
+}
+.fa-mars:before {
+ content: "\f222";
+}
+.fa-mercury:before {
+ content: "\f223";
+}
+.fa-intersex:before,
+.fa-transgender:before {
+ content: "\f224";
+}
+.fa-transgender-alt:before {
+ content: "\f225";
+}
+.fa-venus-double:before {
+ content: "\f226";
+}
+.fa-mars-double:before {
+ content: "\f227";
+}
+.fa-venus-mars:before {
+ content: "\f228";
+}
+.fa-mars-stroke:before {
+ content: "\f229";
+}
+.fa-mars-stroke-v:before {
+ content: "\f22a";
+}
+.fa-mars-stroke-h:before {
+ content: "\f22b";
+}
+.fa-neuter:before {
+ content: "\f22c";
+}
+.fa-genderless:before {
+ content: "\f22d";
+}
+.fa-facebook-official:before {
+ content: "\f230";
+}
+.fa-pinterest-p:before {
+ content: "\f231";
+}
+.fa-whatsapp:before {
+ content: "\f232";
+}
+.fa-server:before {
+ content: "\f233";
+}
+.fa-user-plus:before {
+ content: "\f234";
+}
+.fa-user-times:before {
+ content: "\f235";
+}
+.fa-hotel:before,
+.fa-bed:before {
+ content: "\f236";
+}
+.fa-viacoin:before {
+ content: "\f237";
+}
+.fa-train:before {
+ content: "\f238";
+}
+.fa-subway:before {
+ content: "\f239";
+}
+.fa-medium:before {
+ content: "\f23a";
+}
+.fa-yc:before,
+.fa-y-combinator:before {
+ content: "\f23b";
+}
+.fa-optin-monster:before {
+ content: "\f23c";
+}
+.fa-opencart:before {
+ content: "\f23d";
+}
+.fa-expeditedssl:before {
+ content: "\f23e";
+}
+.fa-battery-4:before,
+.fa-battery-full:before {
+ content: "\f240";
+}
+.fa-battery-3:before,
+.fa-battery-three-quarters:before {
+ content: "\f241";
+}
+.fa-battery-2:before,
+.fa-battery-half:before {
+ content: "\f242";
+}
+.fa-battery-1:before,
+.fa-battery-quarter:before {
+ content: "\f243";
+}
+.fa-battery-0:before,
+.fa-battery-empty:before {
+ content: "\f244";
+}
+.fa-mouse-pointer:before {
+ content: "\f245";
+}
+.fa-i-cursor:before {
+ content: "\f246";
+}
+.fa-object-group:before {
+ content: "\f247";
+}
+.fa-object-ungroup:before {
+ content: "\f248";
+}
+.fa-sticky-note:before {
+ content: "\f249";
+}
+.fa-sticky-note-o:before {
+ content: "\f24a";
+}
+.fa-cc-jcb:before {
+ content: "\f24b";
+}
+.fa-cc-diners-club:before {
+ content: "\f24c";
+}
+.fa-clone:before {
+ content: "\f24d";
+}
+.fa-balance-scale:before {
+ content: "\f24e";
+}
+.fa-hourglass-o:before {
+ content: "\f250";
+}
+.fa-hourglass-1:before,
+.fa-hourglass-start:before {
+ content: "\f251";
+}
+.fa-hourglass-2:before,
+.fa-hourglass-half:before {
+ content: "\f252";
+}
+.fa-hourglass-3:before,
+.fa-hourglass-end:before {
+ content: "\f253";
+}
+.fa-hourglass:before {
+ content: "\f254";
+}
+.fa-hand-grab-o:before,
+.fa-hand-rock-o:before {
+ content: "\f255";
+}
+.fa-hand-stop-o:before,
+.fa-hand-paper-o:before {
+ content: "\f256";
+}
+.fa-hand-scissors-o:before {
+ content: "\f257";
+}
+.fa-hand-lizard-o:before {
+ content: "\f258";
+}
+.fa-hand-spock-o:before {
+ content: "\f259";
+}
+.fa-hand-pointer-o:before {
+ content: "\f25a";
+}
+.fa-hand-peace-o:before {
+ content: "\f25b";
+}
+.fa-trademark:before {
+ content: "\f25c";
+}
+.fa-registered:before {
+ content: "\f25d";
+}
+.fa-creative-commons:before {
+ content: "\f25e";
+}
+.fa-gg:before {
+ content: "\f260";
+}
+.fa-gg-circle:before {
+ content: "\f261";
+}
+.fa-tripadvisor:before {
+ content: "\f262";
+}
+.fa-odnoklassniki:before {
+ content: "\f263";
+}
+.fa-odnoklassniki-square:before {
+ content: "\f264";
+}
+.fa-get-pocket:before {
+ content: "\f265";
+}
+.fa-wikipedia-w:before {
+ content: "\f266";
+}
+.fa-safari:before {
+ content: "\f267";
+}
+.fa-chrome:before {
+ content: "\f268";
+}
+.fa-firefox:before {
+ content: "\f269";
+}
+.fa-opera:before {
+ content: "\f26a";
+}
+.fa-internet-explorer:before {
+ content: "\f26b";
+}
+.fa-tv:before,
+.fa-television:before {
+ content: "\f26c";
+}
+.fa-contao:before {
+ content: "\f26d";
+}
+.fa-500px:before {
+ content: "\f26e";
+}
+.fa-amazon:before {
+ content: "\f270";
+}
+.fa-calendar-plus-o:before {
+ content: "\f271";
+}
+.fa-calendar-minus-o:before {
+ content: "\f272";
+}
+.fa-calendar-times-o:before {
+ content: "\f273";
+}
+.fa-calendar-check-o:before {
+ content: "\f274";
+}
+.fa-industry:before {
+ content: "\f275";
+}
+.fa-map-pin:before {
+ content: "\f276";
+}
+.fa-map-signs:before {
+ content: "\f277";
+}
+.fa-map-o:before {
+ content: "\f278";
+}
+.fa-map:before {
+ content: "\f279";
+}
+.fa-commenting:before {
+ content: "\f27a";
+}
+.fa-commenting-o:before {
+ content: "\f27b";
+}
+.fa-houzz:before {
+ content: "\f27c";
+}
+.fa-vimeo:before {
+ content: "\f27d";
+}
+.fa-black-tie:before {
+ content: "\f27e";
+}
+.fa-fonticons:before {
+ content: "\f280";
+}
diff --git a/src/mahjong_ranking/admin.py b/src/mahjong_ranking/admin.py
index 1deb307..7db8813 100644
--- a/src/mahjong_ranking/admin.py
+++ b/src/mahjong_ranking/admin.py
@@ -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")
diff --git a/src/mahjong_ranking/management/commands/exportranking.py b/src/mahjong_ranking/management/commands/exportranking.py
index d7440a3..b6e3dbd 100644
--- a/src/mahjong_ranking/management/commands/exportranking.py
+++ b/src/mahjong_ranking/management/commands/exportranking.py
@@ -56,7 +56,7 @@ def export_season_rankings(workbook, until):
def export_kyu_dan_rankings(workbook, until):
- KyuDanRanking.objects.update(until=until)
+ KyuDanRanking.objects.update(until=until, force_recalc=True)
object_list = KyuDanRanking.objects.all()
title = "Kyū & Dan Rankings"
columns_settings = (
diff --git a/src/mahjong_ranking/models.py b/src/mahjong_ranking/models.py
index aa44006..8e4a6d1 100644
--- a/src/mahjong_ranking/models.py
+++ b/src/mahjong_ranking/models.py
@@ -566,11 +566,15 @@ 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
diff --git a/src/mahjong_ranking/views.py b/src/mahjong_ranking/views.py
index 275e99c..e39abdf 100644
--- a/src/mahjong_ranking/views.py
+++ b/src/mahjong_ranking/views.py
@@ -240,7 +240,7 @@ class PlayerDanScore(PlayerScore):
{'col': 'L', 'title': 'Dan Punkte', 'attr': 'dan_points',
'style': 'Integer', 'width': 12,
'footer': self.kyu_dan_ranking.legacy_dan_points},
- {'col': 'M', 'title': 'Anmerkung', 'attr': 'comment',
+ {'col': 'M', 'title': 'Anmerkung', 'attr': 'player_comment',
'style': 'Content', 'width': 20, 'footer': 'Legacy Dan Punkte'},
)
diff --git a/src/utils/middleware.py b/src/utils/middleware.py
index 15d8f6a..88dc745 100644
--- a/src/utils/middleware.py
+++ b/src/utils/middleware.py
@@ -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
From afd163298c4b3bcaab9e0f7ae523f95b960f1d68 Mon Sep 17 00:00:00 2001
From: Xeniac
Date: Wed, 10 Jan 2018 02:02:35 +0100
Subject: [PATCH 18/19] * Throw 404 instead of a 503 in Event Mixins if the
related event does not exist. * Changes in the KyuDanRanking View to be more
stable if we get bogus kwargs.
---
src/events/mixins.py | 15 ++--
src/mahjong_ranking/forms.py | 6 ++
.../mahjong_ranking/eventhanchan_form.html | 69 +++++++++++++++++++
src/mahjong_ranking/urls.py | 10 +--
src/mahjong_ranking/views.py | 52 ++++++++++++--
5 files changed, 135 insertions(+), 17 deletions(-)
create mode 100755 src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_form.html
diff --git a/src/events/mixins.py b/src/events/mixins.py
index 914cc55..d9698ab 100644
--- a/src/events/mixins.py
+++ b/src/events/mixins.py
@@ -1,6 +1,6 @@
"""Mixins for Events."""
from django.http import Http404
-
+from django.shortcuts import get_object_or_404
from . import models
@@ -34,12 +34,14 @@ class EventDetailMixin(object):
:return: TemplateContext object"""
context = super(EventDetailMixin, self).get_context_data(**kwargs)
- if hasattr(self, 'event') and self.event:
+ if hasattr(self, 'event'):
context['event'] = self.event
elif hasattr(self, 'object') and isinstance(self.object, models.Event):
context['event'] = self.object
elif hasattr(self, 'object') and hasattr(self.object, 'event'):
context['event'] = self.object.event
+ else:
+ print("No Event in Context!")
return context
def get_queryset(self):
@@ -49,12 +51,9 @@ class EventDetailMixin(object):
:return: a django QuerySets
"""
if self.model == models.Event:
- self.event = models.Event.objects.get(pk=self.kwargs['pk'])
+ self.event = get_object_or_404(models.Event, 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'))
+ self.event = get_object_or_404(models.Event, pk=self.kwargs['event'])
+ queryset = self.model.objects.filter(event=self.event)
return queryset.prefetch_related()
diff --git a/src/mahjong_ranking/forms.py b/src/mahjong_ranking/forms.py
index c852558..4d15cad 100644
--- a/src/mahjong_ranking/forms.py
+++ b/src/mahjong_ranking/forms.py
@@ -10,6 +10,7 @@ from django import forms
from django.utils.translation import ugettext as _
from . import models
+from events.models import Event
USER_MODEL = get_user_model()
@@ -58,3 +59,8 @@ class HanchanAdminForm(HanchanForm):
""" Extend the formfields to add the confirmed checkbox. """
model = models.Hanchan
fields = HanchanForm.Meta.fields + ('confirmed',)
+
+HanchanFormset = forms.inlineformset_factory(Event, models.Hanchan,
+ form=HanchanForm,
+ extra=1,
+ can_delete=True)
diff --git a/src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_form.html b/src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_form.html
new file mode 100755
index 0000000..de744d9
--- /dev/null
+++ b/src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_form.html
@@ -0,0 +1,69 @@
+{% extends "events/event_detail.html" %}{% load i18n humanize thumbnail %}
+
+{% block title %}Hanchans: {{ event.name }}{% endblock %}
+
+{% block maincontent %}{% trans 'Edit Hanchans' %}
+
+
+{% endblock %}
+
+{% block comments %}{% endblock %}{% block buttonbar %}{% endblock %}
diff --git a/src/mahjong_ranking/urls.py b/src/mahjong_ranking/urls.py
index 8ec9615..4158f5c 100644
--- a/src/mahjong_ranking/urls.py
+++ b/src/mahjong_ranking/urls.py
@@ -4,14 +4,15 @@ from django.conf.urls import url
from django.views.generic import RedirectView
from . import views
-
urlpatterns = [ # Ignore PyLintBear (C0103)
url(r'^$',
RedirectView.as_view(url='/ranking/mahjong-ladder/', permanent=True)),
- url(r'^event/(?P[\d]+)/mahjong/$',
- views.EventHanchanList.as_view(), name="event-hanchan-list"),
url(r'^event/(?P[\d]+)/add-hanchan/$',
views.HanchanForm.as_view(), name="add-hanchan-form"),
+ url(r'^event/(?P[\d]+)/edit/$',
+ views.EventHanchanForm.as_view(), name="event-hanchan-form"),
+ url(r'^event/(?P[\d]+)/mahjong/$',
+ views.EventHanchanList.as_view(), name="event-hanchan-list"),
url(r'^event/(?P[\d]+)/mahjong-ranking/$',
views.EventRankingList.as_view(), name="event-ranking"),
url(r'^hanchan/(?P[\d]+)/edit/$',
@@ -32,6 +33,7 @@ urlpatterns = [ # Ignore PyLintBear (C0103)
views.PlayerLadderScore.as_view(), name="player-ladder-score"),
url(r'^mahjong/$', views.KyuDanRankingList.as_view(),
name="kyudanranking-list"),
- url(r'^mahjong/(?P[\+\-\w]+)/$',
+ url(r'^mahjong/(?P[\+\-][a-z_]+)/$',
views.KyuDanRankingList.as_view(), name="kyudanranking-list"),
]
+
diff --git a/src/mahjong_ranking/views.py b/src/mahjong_ranking/views.py
index e39abdf..1c305b1 100644
--- a/src/mahjong_ranking/views.py
+++ b/src/mahjong_ranking/views.py
@@ -16,6 +16,7 @@ from kasu import xlsx
from . import forms, models
from .mixins import MahjongMixin
+DEFAULT_KYU_DAN_ORDER = '-score'
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'),
@@ -107,6 +108,47 @@ class HanchanForm(SuccessMessageMixin, EventDetailMixin,
'one.') % self.object
+class EventHanchanForm(EventDetailMixin, PermissionRequiredMixin,
+ generic.TemplateView):
+ """Display a Formset to add and Edit Hanchans of the specific Event."""
+ permission_required = 'mahjong_ranking.edit_hanchan'
+ template_name = 'mahjong_ranking/eventhanchan_form.html'
+ model=models.Hanchan
+
+ def get_context_data(self, **kwargs):
+ self.event = models.Event.objects.get(pk=self.kwargs['event'])
+ context = super(EventHanchanForm, self).get_context_data()
+ context['formset'] = self.formset
+ return context
+
+ def get(self, request, *args, **kwargs):
+ self.get_queryset()
+ self.formset = forms.HanchanFormset(
+ instance=self.event,
+ initial=[{'start': self.event.start}]
+ )
+ context = self.get_context_data(**kwargs)
+ return self.render_to_response(context)
+
+ def post(self, request, *args, **kwargs):
+ print("ICH WURDE GEPOSTET!!!!")
+ self.get_queryset()
+ self.formset = forms.HanchanFormset(
+ self.request.POST,
+ self.request.FILES,
+ instance=self.event,
+ initial=[{'start': self.event.start}]
+ )
+ if self.formset.is_valid():
+ self.formset.save()
+ return django.http.HttpResponseRedirect(
+ reverse('event-hanchan-form', kwargs={'event': self.event.pk})
+ )
+ context = self.get_context_data(**kwargs)
+ return self.render_to_response(context)
+
+
+
class EventHanchanList(EventDetailMixin, generic.ListView):
"List all hanchans played on a given event."
model = models.Hanchan
@@ -120,15 +162,15 @@ class EventRankingList(EventDetailMixin, generic.ListView):
class KyuDanRankingList(MahjongMixin, generic.ListView):
"""List all Players with an Kyu or Dan score. """
- default_order = '-score'
- order_by = ''
+ order_by = None
paginate_by = 25
def dispatch(self, request, *args, **kwargs):
"""Set the order_by settings, revert to default_order if necessary."""
- self.order_by = KYU_DAN_ORDER[
- kwargs.get('order_by', self.default_order)
- ]
+ if kwargs.get('order_by') in KYU_DAN_ORDER.keys():
+ self.order_by = KYU_DAN_ORDER[kwargs.get('order_by')]
+ else:
+ self.order_by = KYU_DAN_ORDER[DEFAULT_KYU_DAN_ORDER]
return super(KyuDanRankingList, self).dispatch(request, *args, **kwargs)
def get_queryset(self):
From a2df81d62ef706e78e7102668d3b12f14d21b923 Mon Sep 17 00:00:00 2001
From: Xeniac
Date: Wed, 17 Jan 2018 16:19:48 +0100
Subject: [PATCH 19/19] Fixed: Changed the environ to ORIGINAL_RECIPIENT
---
Gruntfile.js | 11 +-
package.json | 2 +-
src/content/locale/de/LC_MESSAGES/django.po | 177 +++----
src/events/locale/de/LC_MESSAGES/django.po | 219 ++++----
src/events/templates/events/event_list.html | 1 -
src/kasu/locale/de/LC_MESSAGES/django.po | 99 ++--
src/kasu/static/css/kasu.css | 4 +
src/kasu/static/js/jquery.formset.js | 231 ++++++++
src/kasu/static/less/common.less | 3 +-
src/mahjong_ranking/forms.py | 16 +-
.../locale/de/LC_MESSAGES/django.po | 447 ++++++++--------
.../mahjong_ranking/eventhanchan_form.html | 83 ++-
.../mahjong_ranking/eventhanchan_list.html | 3 +-
src/mahjong_ranking/views.py | 2 +-
.../locale/de/LC_MESSAGES/django.po | 134 ++---
.../locale/de/LC_MESSAGES/django.po | 359 ++++++-------
src/utils/locale/de/LC_MESSAGES/django.po | 500 +++++++++---------
17 files changed, 1297 insertions(+), 994 deletions(-)
create mode 100644 src/kasu/static/js/jquery.formset.js
diff --git a/Gruntfile.js b/Gruntfile.js
index 55ce42a..f7edc13 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -8,7 +8,7 @@ module.exports = function(grunt) {
options: {
paths: ['src/kasu/static/less'],
compress: false,
- optimization:9,
+ optimization: 9,
ieCompat: false,
},
kasu: {
@@ -21,10 +21,11 @@ module.exports = function(grunt) {
report: 'min'
},
kasu: {
- src: 'static/css/kasu.css',
- dest: 'static/css/kasu.css'
- }
- },
+ files: {
+ 'src/kasu/static/css/kasu.min.css': ['src/kasu/static/css/kasu.css'],
+ },
+ },
+ },
watch: {
styles: {
files: ['src/kasu/static/less/*.less'], // which files to watch
diff --git a/package.json b/package.json
index 7fac8e7..238c86f 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
"grunt": ">=0.4.5",
"grunt-contrib-less": ">=1.0.1",
"grunt-contrib-watch": ">=0.6.1",
- "grunt-more-css": ">=0.1.0"
+ "grunt-more-css": "^0.1.1"
},
"dependencies": {
"ckeditor-dev": "git://github.com/ckeditor/ckeditor-dev.git"
diff --git a/src/content/locale/de/LC_MESSAGES/django.po b/src/content/locale/de/LC_MESSAGES/django.po
index 2193de5..f61f37f 100644
--- a/src/content/locale/de/LC_MESSAGES/django.po
+++ b/src/content/locale/de/LC_MESSAGES/django.po
@@ -8,271 +8,270 @@ msgid ""
msgstr ""
"Project-Id-Version: kasu.content\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-06-19 22:46+0200\n"
-"PO-Revision-Date: 2016-09-28 00:24+0200\n"
-"Last-Translator: Christian Berg \n"
+"POT-Creation-Date: 2018-01-11 22:50+0100\n"
+"PO-Revision-Date: 2018-01-12 15:25+0105\n"
+"Last-Translator: b'Christian Berg '\n"
"Language-Team: Deutsch <>\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-"X-Translated-Using: django-rosetta 0.7.2\n"
"X-Generator: Poedit 1.8.9\n"
+"X-Translated-Using: django-rosetta 0.7.14\n"
-#: content/feeds.py:16
+#: src/content/feeds.py:18
msgid "Current news from Kasu"
msgstr "Aktuelle Nachrichten von Kasu"
-#: content/feeds.py:43
+#: src/content/feeds.py:51
msgid "Latest comments on kasu.at"
msgstr "Neueste Kommentare auf Kasu.at "
-#: content/feeds.py:44
+#: src/content/feeds.py:52
msgid "Kasu - latest comments"
msgstr "Kasu - neue Kommentare"
-#: content/forms.py:52 content/models.py:308
+#: src/content/forms.py:57 src/content/models.py:318
msgid "Please upload a PDF-File to this PDF-Page."
msgstr "Bitte eine PDF Datei für diese PDF Seite hochladen."
-#: content/models.py:62
+#: src/content/models.py:76
msgid "Headline"
msgstr "Schlagzeile"
-#: content/models.py:64
+#: src/content/models.py:78
msgid "Content"
msgstr "Inhalt"
-#: content/models.py:66 content/models.py:137
-#: content/templates/content/article_detail.html:25
+#: src/content/models.py:82 src/content/models.py:150
+#: src/content/templates/content/article_detail.html:25
msgid "Category"
msgstr "Kategorie"
-#: content/models.py:67 content/models.py:130
+#: src/content/models.py:83 src/content/models.py:143
msgid "Image"
msgstr "Bild"
-#: content/models.py:69 content/models.py:132
+#: src/content/models.py:85 src/content/models.py:145
msgid "Slug"
msgstr "Slug"
-#: content/models.py:71 content/templates/content/article_detail.html:23
+#: src/content/models.py:88
+#: src/content/templates/content/article_detail.html:23
msgid "Author"
msgstr "Autor"
-#: content/models.py:72
+#: src/content/models.py:89
msgid "Status"
msgstr "Status"
-#: content/models.py:74
+#: src/content/models.py:91
msgid "Created"
msgstr "Erstellt"
-#: content/models.py:75
+#: src/content/models.py:92
msgid "Modified"
msgstr "Bearbeitet"
-#: content/models.py:80
+#: src/content/models.py:97
msgid "Article"
msgstr "Artikel"
-#: content/models.py:81
+#: src/content/models.py:98
msgid "Articles"
msgstr "Artikel"
-#: content/models.py:126 content/models.py:127
+#: src/content/models.py:139 src/content/models.py:140
msgid "Name"
msgstr "Name"
-#: content/models.py:128 content/models.py:129
+#: src/content/models.py:141 src/content/models.py:142
msgid "Description"
msgstr "Beschreibung"
-#: content/models.py:138
+#: src/content/models.py:151
msgid "Categories"
msgstr "Kategorien"
-#: content/models.py:169 content/models.py:175
+#: src/content/models.py:182 src/content/models.py:188
msgid "The short name for the menu-entry of this page"
msgstr "Ein kurzer Name für den Menüeintrag"
-#: content/models.py:180 content/models.py:185
+#: src/content/models.py:193 src/content/models.py:198
msgid "The page title as you'd like it to be seen by the public"
-msgstr ""
+msgstr "Der Seitentitel der öffentlich angezeigt werden soll"
-#: content/models.py:187
+#: src/content/models.py:200
msgid "slug"
msgstr "Slug"
-#: content/models.py:190
+#: src/content/models.py:203
msgid ""
-"The name of the page as it will appear in URLs e.g http://domain.com/blog/"
-"[my-slug]/"
+"The name of the page as it will appear in URLs e.g "
+"http://domain.com/blog/[my-slug]/"
msgstr ""
+"Wie die Seite in der URL aufscheint also http://domain.com/blog/[slug]"
-#: content/models.py:199
+#: src/content/models.py:212
msgid "Path"
msgstr "Pfad"
-#: content/models.py:211
+#: src/content/models.py:224
msgid "Position"
msgstr "Position"
-#: content/models.py:216
+#: src/content/models.py:229
msgid "status"
msgstr "Status"
-#: content/models.py:219 content/models.py:221
-#, fuzzy
+#: src/content/models.py:232 src/content/models.py:234
#| msgid "Description"
msgid "search description"
-msgstr "Beschreibung"
+msgstr "Beschreibung für Suchfunktion"
-#: content/models.py:224
-#, fuzzy
+#: src/content/models.py:237
#| msgid "Content"
msgid "content type"
-msgstr "Inhalt"
+msgstr "Inhaltstyp"
-#: content/models.py:229
+#: src/content/models.py:242
msgid "enable comments"
msgstr "Kommentare möglich"
-#: content/models.py:234
+#: src/content/models.py:247
msgid "Template"
msgstr "Vorlage"
-#: content/models.py:242
-#, fuzzy
+#: src/content/models.py:255
#| msgid "created on"
msgid "first created at"
msgstr "erstellt am"
-#: content/models.py:247
+#: src/content/models.py:260
msgid "latest updated at"
-msgstr ""
+msgstr "letzte Aktualisierung am"
-#: content/models.py:322
+#: src/content/models.py:331
msgid "Page"
msgstr "Seite"
-#: content/models.py:323
+#: src/content/models.py:332
msgid "Pages"
msgstr "Seiten"
-#: content/templates/content/article_archive.html:5
-#: content/templates/content/article_archive.html:20
+#: src/content/templates/content/article_archive.html:5
+#: src/content/templates/content/article_archive.html:20
msgid "Article Archive"
msgstr "Nachrichtenarchiv"
-#: content/templates/content/article_archive.html:35
-#: content/templates/content/article_archive_month.html:5
-#: content/templates/content/article_archive_year.html:7
+#: src/content/templates/content/article_archive.html:35
+#: src/content/templates/content/article_archive_month.html:5
+#: src/content/templates/content/article_archive_year.html:7
msgid "Archive"
msgstr "Archiv"
-#: content/templates/content/article_archive.html:56
+#: src/content/templates/content/article_archive.html:56
msgid "All Categories"
msgstr "Alle Kategorien"
-#: content/templates/content/article_archive.html:71
+#: src/content/templates/content/article_archive.html:71
msgid "created on"
msgstr "erstellt am"
-#: content/templates/content/article_archive.html:73
+#: src/content/templates/content/article_archive.html:73
msgid "by"
msgstr "von"
-#: content/templates/content/article_archive.html:74
-#: content/templates/content/article_archive.html:75
+#: src/content/templates/content/article_archive.html:74
+#: src/content/templates/content/article_archive.html:75
msgid "comments"
msgstr "Kommentare"
-#: content/templates/content/article_archive.html:81
+#: src/content/templates/content/article_archive.html:81
msgid "Read More"
msgstr "Mehr lesen"
-#: content/templates/content/article_archive.html:86
+#: src/content/templates/content/article_archive.html:86
msgid "We're sorry. Your search yielded no results."
msgstr "Es tut uns leid. Deine Suche ergab keine Treffer."
-#: content/templates/content/article_archive.html:104
+#: src/content/templates/content/article_archive.html:104
msgid "Add Article"
msgstr "neuer Artikel "
-#: content/templates/content/article_archive_month.html:7
+#: src/content/templates/content/article_archive_month.html:7
msgid "back"
msgstr "Zurück"
-#: content/templates/content/article_detail.html:24
+#: src/content/templates/content/article_detail.html:24
msgid "Created on"
msgstr "Erstellt am"
-#: content/templates/content/article_detail.html:36
+#: src/content/templates/content/article_detail.html:36
msgid "share on"
msgstr "Teile auf"
-#: content/templates/content/article_detail.html:51
-#: content/templates/content/article_form.html:20 content/views.py:138
+#: src/content/templates/content/article_detail.html:51
+#: src/content/views.py:156
msgid "Edit Article"
msgstr "Artikel bearbeiten"
-#: content/templates/content/article_form.html:20 content/views.py:139
-msgid "Create Article"
-msgstr "Artikel erstellen"
-
-#: content/templates/content/article_form.html:25
-#: content/templates/content/page_form.html:49
-#: content/templates/content/page_form.html:56
+#: src/content/templates/content/article_form.html:32
+#: src/content/templates/content/page_form.html:42
+#: src/content/templates/content/page_form.html:49
msgid "German"
msgstr "Deutsch"
-#: content/templates/content/article_form.html:26
-#: content/templates/content/page_form.html:50
-#: content/templates/content/page_form.html:64
+#: src/content/templates/content/article_form.html:33
+#: src/content/templates/content/page_form.html:43
+#: src/content/templates/content/page_form.html:57
msgid "English"
msgstr "Englisch"
-#: content/templates/content/article_form.html:45
-#: content/templates/content/page_form.html:73
+#: src/content/templates/content/article_form.html:59
+#: src/content/templates/content/page_form.html:66
msgid "reset"
msgstr "Zurücksetzen"
-#: content/templates/content/article_form.html:46
-#: content/templates/content/page_form.html:74
+#: src/content/templates/content/article_form.html:60
+#: src/content/templates/content/page_form.html:67
msgid "save"
msgstr "Speichern"
-#: content/templates/content/page_form.html:5
-#: content/templates/content/page_form.html:42
+#: src/content/templates/content/page_form.html:5
+#: src/content/templates/content/page_form.html:35
msgid "Edit Page"
msgstr "Seite bearbeiten"
-#: content/templates/content/page_form.html:5
-#: content/templates/content/page_form.html:28
-#: content/templates/content/page_form.html:42
+#: src/content/templates/content/page_form.html:5
+#: src/content/templates/content/page_form.html:19
+#: src/content/templates/content/page_form.html:35
msgid "Add Page"
msgstr "Seite hinzufügen"
-#: content/templates/content/page_form.html:27
+#: src/content/templates/content/page_form.html:18
msgid "Edit"
msgstr "Bearbeiten"
-#: content/templates/content/page_form.html:44
+#: src/content/templates/content/page_form.html:37
msgid "HTML Specific"
msgstr "HTML spezifisch"
-#: content/views.py:35
+#: src/content/views.py:53
msgid "This Category does not exist."
msgstr "Diese Kategorie existiert nicht."
-#: content/views.py:205
+#: src/content/views.py:157
+msgid "Create Article"
+msgstr "Artikel erstellen"
+
+#: src/content/views.py:233
#, python-format
msgid "No Page found matching the Path %s"
msgstr "Keine Seite unter dem Pfad %s gefunden"
-#: content/views.py:219
+#: src/content/views.py:262
#, python-format
msgid "No PDF Document found matching the Path %s"
msgstr "Kein PDF Dokument unter dem Pfad %s gefunden."
diff --git a/src/events/locale/de/LC_MESSAGES/django.po b/src/events/locale/de/LC_MESSAGES/django.po
index 74dd00b..5652376 100644
--- a/src/events/locale/de/LC_MESSAGES/django.po
+++ b/src/events/locale/de/LC_MESSAGES/django.po
@@ -7,9 +7,9 @@ msgid ""
msgstr ""
"Project-Id-Version: kasu.events\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-06-19 22:46+0200\n"
-"PO-Revision-Date: 2016-09-28 00:24+0200\n"
-"Last-Translator: Christian Berg \n"
+"POT-Creation-Date: 2018-01-11 22:50+0100\n"
+"PO-Revision-Date: 2018-01-12 15:25+0105\n"
+"Last-Translator: b'Christian Berg '\n"
"Language-Team: Kasu \n"
"Language: de\n"
"MIME-Version: 1.0\n"
@@ -17,60 +17,60 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 1.8.9\n"
-"X-Translated-Using: django-rosetta 0.7.6\n"
+"X-Translated-Using: django-rosetta 0.7.14\n"
-#: events/admin.py:14 events/models.py:82
+#: src/events/admin.py:14 src/events/models.py:82
msgid "Event Series"
msgstr "Veranstaltungsreihen"
-#: events/forms.py:18
+#: src/events/forms.py:17
msgid "Images"
msgstr "Bilder"
-#: events/forms.py:46
+#: src/events/forms.py:46
msgid "start"
msgstr "Beginn"
-#: events/forms.py:50
+#: src/events/forms.py:49
msgid "end"
msgstr "Ende"
-#: events/models.py:52 events/models.py:176 events/models.py:217
+#: src/events/models.py:52 src/events/models.py:176 src/events/models.py:217
msgid "Name"
msgstr "Name"
-#: events/models.py:53 events/models.py:177 events/models.py:225
+#: src/events/models.py:53 src/events/models.py:177 src/events/models.py:225
msgid "Description"
msgstr "Beschreibung"
-#: events/models.py:55 events/templates/events/event_detail.html:29
-#: events/templates/events/event_detail.html:87
-#: events/templates/events/event_list.html:28
-#: events/templates/events/photo_upload.html:13
+#: src/events/models.py:55 src/events/templates/events/event_detail.html:29
+#: src/events/templates/events/event_detail.html:87
+#: src/events/templates/events/event_list.html:28
+#: src/events/templates/events/photo_upload.html:13
msgid "Start"
msgstr "Beginn"
-#: events/models.py:56 events/templates/events/event_detail.html:30
-#: events/templates/events/event_detail.html:89
+#: src/events/models.py:56 src/events/templates/events/event_detail.html:30
+#: src/events/templates/events/event_detail.html:89
msgid "End"
msgstr "Ende"
-#: events/models.py:57 events/models.py:185
-#: events/templates/events/event_detail.html:34
-#: events/templates/events/event_detail.html:80
-#: events/templates/events/event_detail.html:92
+#: src/events/models.py:57 src/events/models.py:185
+#: src/events/templates/events/event_detail.html:34
+#: src/events/templates/events/event_detail.html:80
+#: src/events/templates/events/event_detail.html:92
msgid "Homepage"
msgstr "Homepage"
-#: events/models.py:59 events/models.py:179 events/models.py:219
+#: src/events/models.py:59 src/events/models.py:179 src/events/models.py:219
msgid "Image"
msgstr "Bild"
-#: events/models.py:66
+#: src/events/models.py:66
msgid "Mahjong Tournament"
msgstr "Mahjong Turnier"
-#: events/models.py:68
+#: src/events/models.py:68
msgid ""
"This event is a tournament, different rules apply for the kyu "
"ranking."
@@ -78,11 +78,11 @@ msgstr ""
"Diese Veranstaltung ist ein Turnier, es gelten andere Regeln für das Kyu "
"Ranking."
-#: events/models.py:72
+#: src/events/models.py:72
msgid "Mahjong Season"
msgstr "Mahjong Saison"
-#: events/models.py:83
+#: src/events/models.py:83
msgid ""
"Wenn dieser Event zu einer Veranstaltungsreihe gehört werden Ort, "
"Beschreibung, Bild und Homepage von dem hier angegebenen Event "
@@ -91,244 +91,245 @@ msgstr ""
"Wenn dieser Termin zu einer Veranstaltungsreihe gehört werden Ort, "
"Beschreibung, Bild und Homepage von dem hier angegebenen Event übernommen."
-#: events/models.py:92 events/models.py:195 events/models.py:247
+#: src/events/models.py:92 src/events/models.py:195 src/events/models.py:248
msgid "first created at"
-msgstr ""
+msgstr "erstellt am"
-#: events/models.py:97 events/models.py:200 events/models.py:252
+#: src/events/models.py:97 src/events/models.py:200 src/events/models.py:253
msgid "latest updated at"
-msgstr ""
+msgstr "letzte Aktualisierung am"
-#: events/models.py:103
+#: src/events/models.py:103
msgid "Event"
msgstr "Termin"
-#: events/models.py:104
+#: src/events/models.py:104
msgid "Events"
msgstr "Termine"
-#: events/models.py:117
+#: src/events/models.py:117
msgid "A event can't end before it had started"
msgstr "Eine Veranstaltung kann nicht enden bevor sie begonnen hat"
-#: events/models.py:186
+#: src/events/models.py:186
msgid "Postal Code"
msgstr "Postleitzahl"
-#: events/models.py:187
+#: src/events/models.py:187
msgid "Street Address"
msgstr "Straße"
-#: events/models.py:188
+#: src/events/models.py:188
msgid "Locality"
msgstr "Ort"
-#: events/models.py:189
+#: src/events/models.py:189
msgid "Country"
msgstr "Land"
-#: events/models.py:204
+#: src/events/models.py:204
msgid "Venue"
msgstr "Veranstaltungsort"
-#: events/models.py:205
+#: src/events/models.py:205
msgid "Venues"
msgstr "Veranstaltungsorte"
-#: events/models.py:231
+#: src/events/models.py:232
msgid "Startpage"
msgstr "Startseite"
-#: events/models.py:234
+#: src/events/models.py:235
msgid "Display this Photo on the Startpage Teaser"
msgstr "Foto als Teaser auf der Startseite verwenden."
-#: events/models.py:236
+#: src/events/models.py:237
msgid "Published on"
msgstr "Veröffentlicht am"
-#: events/models.py:238
+#: src/events/models.py:239
msgid "Number of views"
msgstr "Wie oft gesehen"
-#: events/models.py:262 events/templates/events/event_archive.html:38
-#: events/templates/events/event_list.html:18
+#: src/events/models.py:263 src/events/templates/events/event_archive.html:38
+#: src/events/templates/events/event_list.html:18
msgid "Event Image"
msgstr "Veranstaltungsbild"
-#: events/models.py:263
+#: src/events/models.py:264
msgid "Event Images"
msgstr "Veranstaltungsbilder"
-#: events/templates/events/event_archive.html:5
-#: events/templates/events/event_archive.html:9
+#: src/events/templates/events/event_archive.html:5
+#: src/events/templates/events/event_archive.html:9
msgid "Event Archive"
msgstr "Veranstaltungsarchiv"
-#: events/templates/events/event_archive.html:42
-#: events/templates/events/event_detail.html:85
-#: events/templates/events/event_list.html:22
-#: events/templates/events/photo_detail.html:53
+#: src/events/templates/events/event_archive.html:42
+#: src/events/templates/events/event_detail.html:85
+#: src/events/templates/events/event_list.html:22
+#: src/events/templates/events/photo_detail.html:53
msgid "Date"
msgstr "Datum"
-#: events/templates/events/event_archive.html:47
+#: src/events/templates/events/event_archive.html:47
msgid "Time"
msgstr "Zeit"
-#: events/templates/events/event_archive.html:49
-#: events/templates/events/photo_upload.html:16
+#: src/events/templates/events/event_archive.html:49
+#: src/events/templates/events/photo_upload.html:16
msgid "from"
msgstr "von"
-#: events/templates/events/event_archive.html:49
-#: events/templates/events/photo_upload.html:16
+#: src/events/templates/events/event_archive.html:49
+#: src/events/templates/events/photo_upload.html:16
msgid "to"
msgstr "bis"
-#: events/templates/events/event_archive.html:57
-#: events/templates/events/event_detail.html:31
-#: events/templates/events/event_detail.html:72
-#: events/templates/events/event_list.html:32
-#: events/templates/events/photo_upload.html:23
+#: src/events/templates/events/event_archive.html:57
+#: src/events/templates/events/event_detail.html:31
+#: src/events/templates/events/event_detail.html:72
+#: src/events/templates/events/event_list.html:32
+#: src/events/templates/events/photo_upload.html:23
msgid "Location"
msgstr "Ort"
-#: events/templates/events/event_archive.html:58
-#: events/templates/events/event_list.html:35
-#: events/templates/events/photo_upload.html:25
-#: events/templates/events/photo_upload.html:26
+#: src/events/templates/events/event_archive.html:58
+#: src/events/templates/events/event_list.html:35
+#: src/events/templates/events/photo_upload.html:25
+#: src/events/templates/events/photo_upload.html:26
msgid "Comments"
msgstr "Kommentare"
-#: events/templates/events/event_archive.html:59
-#: events/templates/events/event_detail.html:36
-#: events/templates/events/event_detail.html:48
-#: events/templates/events/photo_upload.html:28
-#: events/templates/events/photo_upload.html:29
+#: src/events/templates/events/event_archive.html:59
+#: src/events/templates/events/event_detail.html:36
+#: src/events/templates/events/event_detail.html:48
+#: src/events/templates/events/photo_list.html:4
+#: src/events/templates/events/photo_upload.html:28
+#: src/events/templates/events/photo_upload.html:29
msgid "Photos"
msgstr "Fotos"
-#: events/templates/events/event_archive.html:60
-#: events/templates/events/event_archive.html:61
-#: events/templates/events/event_detail.html:35
-#: events/templates/events/event_detail.html:51
+#: src/events/templates/events/event_archive.html:60
+#: src/events/templates/events/event_archive.html:61
+#: src/events/templates/events/event_detail.html:35
+#: src/events/templates/events/event_detail.html:51
msgid "Hanchans"
msgstr "Hanchans"
-#: events/templates/events/event_detail.html:37
+#: src/events/templates/events/event_detail.html:37
msgid "tourney"
msgstr "Turnier"
-#: events/templates/events/event_detail.html:37
+#: src/events/templates/events/event_detail.html:37
msgid "other rules apply here"
msgstr "hier gelten andere Regeln"
-#: events/templates/events/event_detail.html:45
+#: src/events/templates/events/event_detail.html:45
msgid "Info"
msgstr "Info"
-#: events/templates/events/event_detail.html:54
+#: src/events/templates/events/event_detail.html:54
msgid "Mai-Star Games"
msgstr "Mai-Star Spiele"
-#: events/templates/events/event_detail.html:57
+#: src/events/templates/events/event_detail.html:57
msgid "Event Ranking"
msgstr "Veranstaltungs Wertung"
-#: events/templates/events/event_detail.html:100
+#: src/events/templates/events/event_detail.html:100
msgid "Share on Facebook"
msgstr "Auf Facebook teilen"
-#: events/templates/events/event_detail.html:104
+#: src/events/templates/events/event_detail.html:104
msgid "Share on Google+"
msgstr "Auf Google+ teilen"
-#: events/templates/events/event_detail.html:109
+#: src/events/templates/events/event_detail.html:109
msgid "Share on Twitter"
msgstr "Auf Twitter teilen"
-#: events/templates/events/event_detail.html:113
+#: src/events/templates/events/event_detail.html:113
msgid "Show on Google Maps"
msgstr "Auf Google Maps zeigen"
-#: events/templates/events/event_detail.html:127
-#: events/templates/events/event_form.html:9 events/views.py:104
+#: src/events/templates/events/event_detail.html:127
+#: src/events/templates/events/event_form.html:9 src/events/views.py:62
msgid "Edit Event"
msgstr "Termin bearbeiten"
-#: events/templates/events/event_detail.html:131
+#: src/events/templates/events/event_detail.html:131
msgid "Add Dates"
msgstr "Termine hinzufügen"
-#: events/templates/events/event_form.html:9
-#: events/templates/events/page.html:9 events/views.py:106
+#: src/events/templates/events/event_form.html:9
+#: src/events/templates/events/page.html:9 src/events/views.py:64
msgid "Add Event"
msgstr "Neuer Termin"
-#: events/templates/events/event_form.html:18
-#: events/templates/events/photo_list.html:35
+#: src/events/templates/events/event_form.html:18
+#: src/events/templates/events/photo_list.html:35
msgid "reset"
msgstr "Zurücksetzen"
-#: events/templates/events/event_form.html:19
-#: events/templates/events/eventseries_form.html:25
+#: src/events/templates/events/event_form.html:19
+#: src/events/templates/events/eventseries_form.html:25
msgid "save"
msgstr "Speichern"
-#: events/templates/events/event_list.html:4
-#: events/templates/events/event_list.html:5
+#: src/events/templates/events/event_list.html:4
+#: src/events/templates/events/event_list.html:5
msgid "Upcoming Events"
msgstr "Bevorstehende Veranstaltungen"
-#: events/templates/events/eventseries_form.html:24
+#: src/events/templates/events/eventseries_form.html:24
msgid "back"
msgstr "Zurück"
-#: events/templates/events/photo_confirm_delete.html:17
+#: src/events/templates/events/photo_confirm_delete.html:17
msgid "Cancel"
msgstr "Abbrechen"
-#: events/templates/events/photo_confirm_delete.html:21
-#: events/templates/events/photo_list.html:21
+#: src/events/templates/events/photo_confirm_delete.html:21
+#: src/events/templates/events/photo_list.html:21
msgid "Delete"
msgstr "Löschen"
-#: events/templates/events/photo_detail.html:44
+#: src/events/templates/events/photo_detail.html:44
msgid "previous"
msgstr "Zurück"
-#: events/templates/events/photo_detail.html:52
+#: src/events/templates/events/photo_detail.html:52
msgid "Photographer"
msgstr "Fotograf"
-#: events/templates/events/photo_detail.html:58
+#: src/events/templates/events/photo_detail.html:58
msgid "share on"
msgstr "Teile auf"
-#: events/templates/events/photo_detail.html:81
+#: src/events/templates/events/photo_detail.html:81
msgid "download"
msgstr "Herunterladen"
-#: events/templates/events/photo_detail.html:82
+#: src/events/templates/events/photo_detail.html:82
msgid "Rotate counter clockwise"
msgstr "mit dem Uhrzeiger drehen"
-#: events/templates/events/photo_detail.html:83
+#: src/events/templates/events/photo_detail.html:83
msgid "Rotate clockwise"
msgstr "gegen den Uhrzeiger drehen"
-#: events/templates/events/photo_detail.html:84
+#: src/events/templates/events/photo_detail.html:84
msgid "Save"
msgstr "Speichern"
-#: events/templates/events/photo_list.html:36
-#: events/templates/events/photo_upload.html:35
-#: events/templates/events/photo_upload.html:49
+#: src/events/templates/events/photo_list.html:36
+#: src/events/templates/events/photo_upload.html:35
+#: src/events/templates/events/photo_upload.html:49
msgid "Upload"
msgstr "Hochladen"
-#: events/views.py:203
+#: src/events/views.py:149
msgid "Event does not exist"
msgstr "Veranstaltung gibt es nicht"
diff --git a/src/events/templates/events/event_list.html b/src/events/templates/events/event_list.html
index a685789..619feda 100755
--- a/src/events/templates/events/event_list.html
+++ b/src/events/templates/events/event_list.html
@@ -41,5 +41,4 @@
{% if forloop.counter|divisibleby:2 %} {% endif %}
{% endfor %}
{% endfor %}
-{% if page_obj.has_other_pages %}{% include 'paginator.html' %}{% endif %}
{% endblock %}
diff --git a/src/kasu/locale/de/LC_MESSAGES/django.po b/src/kasu/locale/de/LC_MESSAGES/django.po
index b690e50..9f2c8dd 100644
--- a/src/kasu/locale/de/LC_MESSAGES/django.po
+++ b/src/kasu/locale/de/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: kasu.utils\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-06-19 22:46+0200\n"
+"POT-Creation-Date: 2018-01-11 22:50+0100\n"
"PO-Revision-Date: 2016-09-28 00:24+0200\n"
"Last-Translator: Christian Berg \n"
"Language-Team: Kasu \n"
@@ -19,174 +19,175 @@ msgstr ""
"X-Generator: Poedit 1.8.9\n"
"X-Translated-Using: django-rosetta 0.7.6\n"
-#: kasu/settings.py:153
+#: src/kasu/settings.py:158
msgid "German"
msgstr "Deutsch"
-#: kasu/settings.py:153
+#: src/kasu/settings.py:158
msgid "English"
msgstr "Englisch"
-#: kasu/templates/404.html:8
+#: src/kasu/templates/404.html:8
msgid "The page your requested does not exist on this server."
msgstr "Die angeforderte Seite existiert auf diesem Server nicht."
-#: kasu/templates/base.html:22
+#: src/kasu/templates/base.html:22
msgid "Current News"
msgstr "Aktuelle Neuigkeiten"
-#: kasu/templates/base.html:24 kasu/templates/index.html:40
+#: src/kasu/templates/base.html:24 src/kasu/templates/index.html:40
msgid "Recent Comments"
msgstr "Kürzliche Kommentare"
-#: kasu/templates/base.html:45
+#: src/kasu/templates/base.html:45
msgid "Menu"
msgstr "Menü"
-#: kasu/templates/base.html:69
+#: src/kasu/templates/base.html:69
msgid "Current Event"
msgstr "Aktuelle Veranstaltung"
-#: kasu/templates/base.html:72
+#: src/kasu/templates/base.html:72
msgid "Since"
msgstr "seit"
-#: kasu/templates/base.html:73 kasu/templates/base.html:85
+#: src/kasu/templates/base.html:73 src/kasu/templates/base.html:85
msgid "Start"
msgstr "Beginn"
-#: kasu/templates/base.html:76 kasu/templates/base.html:88
+#: src/kasu/templates/base.html:76 src/kasu/templates/base.html:88
msgid "Location"
msgstr "Ort"
-#: kasu/templates/base.html:79 kasu/templates/base.html:90
+#: src/kasu/templates/base.html:79 src/kasu/templates/base.html:90
msgid "More Details"
msgstr "Mehr Details"
-#: kasu/templates/base.html:81
+#: src/kasu/templates/base.html:81
msgid "Next Event"
msgstr "Nächste Veranstaltung"
-#: kasu/templates/base.html:84
+#: src/kasu/templates/base.html:84
msgid "in"
msgstr "in"
-#: kasu/templates/base.html:93
+#: src/kasu/templates/base.html:93
msgid "Upcoming events"
msgstr "Bevorstehende Veranstaltungen"
-#: kasu/templates/base.html:143
+#: src/kasu/templates/base.html:143
msgid "Add Subpage"
msgstr "Unterseite Hinzufügen"
-#: kasu/templates/base.html:148
+#: src/kasu/templates/base.html:148
msgid "Edit Page"
msgstr "Seite bearbeiten"
-#: kasu/templates/base.html:156
+#: src/kasu/templates/base.html:156
msgid "Imprint"
msgstr "Impressum"
-#: kasu/templates/base.html:157
+#: src/kasu/templates/base.html:157
msgid "contact"
msgstr "Kontakt"
-#: kasu/templates/base.html:162
+#: src/kasu/templates/base.html:162
msgid "Language"
msgstr "Sprache"
-#: kasu/templates/base.html:171
+#: src/kasu/templates/base.html:171
msgid "Go"
msgstr "Los"
-#: kasu/templates/base.html:176
+#: src/kasu/templates/base.html:176
msgid "Logged in as"
msgstr "Angemeldet als"
-#: kasu/templates/base.html:178
+#: src/kasu/templates/base.html:178
msgid "Admin"
msgstr "Admin"
-#: kasu/templates/base.html:179
+#: src/kasu/templates/base.html:179
msgid "Logout"
msgstr "Abmelden"
-#: kasu/templates/base.html:181
+#: src/kasu/templates/base.html:181
msgid "no user logged in"
msgstr "Niemand angemeldet"
-#: kasu/templates/base.html:182 kasu/templates/comments/form.html:43
+#: src/kasu/templates/base.html:182 src/kasu/templates/comments/form.html:43
msgid "register"
msgstr "Registrieren"
-#: kasu/templates/base.html:183 kasu/templates/comments/form.html:44
+#: src/kasu/templates/base.html:183 src/kasu/templates/comments/form.html:44
msgid "login"
msgstr "anmelden"
-#: kasu/templates/base.html:185
+#: src/kasu/templates/base.html:185
msgid "Login with Facebook"
msgstr "über Facebook anmelden"
-#: kasu/templates/base.html:187
+#: src/kasu/templates/base.html:187
msgid "Login with Twitter"
msgstr "über Twitter anmelden"
-#: kasu/templates/base.html:189
+#: src/kasu/templates/base.html:189
msgid "Login with Google"
msgstr "über Google anmelden"
-#: kasu/templates/comments/form.html:5
+#: src/kasu/templates/comments/form.html:5
msgid "New Comment"
msgstr "Neuer Kommentar"
-#: kasu/templates/comments/form.html:20
+#: src/kasu/templates/comments/form.html:20
msgid "now"
msgstr "Jetzt"
-#: kasu/templates/comments/form.html:25
+#: src/kasu/templates/comments/form.html:25
msgid "Preview"
msgstr "Vorschau"
-#: kasu/templates/comments/form.html:26
+#: src/kasu/templates/comments/form.html:26
msgid "Post"
msgstr "Schreiben"
-#: kasu/templates/comments/form.html:34
+#: src/kasu/templates/comments/form.html:34
msgid "not logged in"
msgstr "Nicht angemeldet"
-#: kasu/templates/comments/form.html:38
+#: src/kasu/templates/comments/form.html:38
msgid "Register now, or Login to leave a comment here."
msgstr "Jetzt registrieren, oder anmelden um einen Kommentar zu schreiben."
-#: kasu/templates/comments/list.html:2 kasu/templates/index.html:25
+#: src/kasu/templates/comments/list.html:2 src/kasu/templates/index.html:25
msgid "Comments"
msgstr "Kommentare"
-#: kasu/templates/comments/posted.html:4 kasu/templates/comments/posted.html:7
+#: src/kasu/templates/comments/posted.html:4
+#: src/kasu/templates/comments/posted.html:7
msgid "Thank you for your comment"
msgstr "Danke für deinen Kommentar."
-#: kasu/templates/comments/preview.html:4
-#: kasu/templates/comments/preview.html:6
+#: src/kasu/templates/comments/preview.html:4
+#: src/kasu/templates/comments/preview.html:6
msgid "Preview your comment"
msgstr "Vorschau deines Kommentars"
-#: kasu/templates/comments/preview.html:10
+#: src/kasu/templates/comments/preview.html:10
msgid "Please correct the error below"
msgid_plural "Please correct the errors below"
msgstr[0] "Bitte den Fehler weiter unten beheben"
msgstr[1] "Bitte die Fehler weiter unten beheben"
-#: kasu/templates/index.html:4
+#: src/kasu/templates/index.html:4
msgid "traditional Asian game culture"
msgstr "traditionelle asiatische Spielkultur"
-#: kasu/templates/index.html:33
+#: src/kasu/templates/index.html:33
msgid "Read More"
msgstr "Mehr lesen"
-#: kasu/templates/index.html:47
+#: src/kasu/templates/index.html:47
#, python-format
msgid ""
"\n"
@@ -203,23 +204,23 @@ msgstr ""
" %(since)s \n"
" "
-#: kasu/templates/index.html:59
+#: src/kasu/templates/index.html:59
msgid "Kasu in the social network"
msgstr "Kasu im sozialem Netzwerk"
-#: kasu/templates/index.html:62 kasu/templates/index.html:65
+#: src/kasu/templates/index.html:62 src/kasu/templates/index.html:65
msgid "Visit us on"
msgstr "Besuche uns auf"
-#: kasu/templates/index.html:74
+#: src/kasu/templates/index.html:74
msgid "Add Article"
msgstr "Artikel hinzufügen"
-#: kasu/templates/paginator.html:8
+#: src/kasu/templates/paginator.html:8
msgid "Previous"
msgstr "Vorherige"
-#: kasu/templates/paginator.html:20
+#: src/kasu/templates/paginator.html:20
msgid "Next"
msgstr "Nächste"
diff --git a/src/kasu/static/css/kasu.css b/src/kasu/static/css/kasu.css
index b889b3e..7c33e26 100644
--- a/src/kasu/static/css/kasu.css
+++ b/src/kasu/static/css/kasu.css
@@ -337,6 +337,10 @@ ul.tabs li.active a {
ul.errorlist li {
color: #a40000;
}
+input.error {
+ border-color: #a40000;
+ background-color: rgba(164, 0, 0, 0.25);
+}
.game h2 {
margin: 0.5em 0;
}
diff --git a/src/kasu/static/js/jquery.formset.js b/src/kasu/static/js/jquery.formset.js
new file mode 100644
index 0000000..d910758
--- /dev/null
+++ b/src/kasu/static/js/jquery.formset.js
@@ -0,0 +1,231 @@
+/**
+ * jQuery Formset 1.3-pre
+ * @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com)
+ * @requires jQuery 1.2.6 or later
+ *
+ * Copyright (c) 2009, Stanislaus Madueke
+ * All rights reserved.
+ *
+ * Licensed under the New BSD License
+ * See: http://www.opensource.org/licenses/bsd-license.php
+ */
+;(function($) {
+ $.fn.formset = function(opts)
+ {
+ var options = $.extend({}, $.fn.formset.defaults, opts),
+ flatExtraClasses = options.extraClasses.join(' '),
+ totalForms = $('#id_' + options.prefix + '-TOTAL_FORMS'),
+ maxForms = $('#id_' + options.prefix + '-MAX_NUM_FORMS'),
+ minForms = $('#id_' + options.prefix + '-MIN_NUM_FORMS'),
+ childElementSelector = 'input,select,textarea,label,div',
+ $$ = $(this),
+
+ applyExtraClasses = function(row, ndx) {
+ if (options.extraClasses) {
+ row.removeClass(flatExtraClasses);
+ row.addClass(options.extraClasses[ndx % options.extraClasses.length]);
+ }
+ },
+
+ updateElementIndex = function(elem, prefix, ndx) {
+ var idRegex = new RegExp(prefix + '-(\\d+|__prefix__)-'),
+ replacement = prefix + '-' + ndx + '-';
+ if (elem.attr("for")) elem.attr("for", elem.attr("for").replace(idRegex, replacement));
+ if (elem.attr('id')) elem.attr('id', elem.attr('id').replace(idRegex, replacement));
+ if (elem.attr('name')) elem.attr('name', elem.attr('name').replace(idRegex, replacement));
+ },
+
+ hasChildElements = function(row) {
+ return row.find(childElementSelector).length > 0;
+ },
+
+ showAddButton = function() {
+ return maxForms.length == 0 || // For Django versions pre 1.2
+ (maxForms.val() == '' || (maxForms.val() - totalForms.val() > 0));
+ },
+
+ /**
+ * Indicates whether delete link(s) can be displayed - when total forms > min forms
+ */
+ showDeleteLinks = function() {
+ return minForms.length == 0 || // For Django versions pre 1.7
+ (minForms.val() == '' || (totalForms.val() - minForms.val() > 0));
+ },
+
+ insertDeleteLink = function(row) {
+ var delCssSelector = $.trim(options.deleteCssClass).replace(/\s+/g, '.'),
+ addCssSelector = $.trim(options.addCssClass).replace(/\s+/g, '.');
+ if (row.is('TR')) {
+ // If the forms are laid out in table rows, insert
+ // the remove button into the last table cell:
+ row.children(':last').append('' + options.deleteText + ' ');
+ } else if (row.is('UL') || row.is('OL')) {
+ // If they're laid out as an ordered/unordered list,
+ // insert an after the last list item:
+ row.append(' ' + options.deleteText +' ');
+ } else {
+ // Otherwise, just insert the remove button as the
+ // last child element of the form's container:
+ row.append('' + options.deleteText +' ');
+ }
+ // Check if we're under the minimum number of forms - not to display delete link at rendering
+ if (!showDeleteLinks()){
+ row.find('a.' + delCssSelector).hide();
+ }
+
+ row.find('a.' + delCssSelector).click(function() {
+ var row = $(this).parents('.' + options.formCssClass),
+ del = row.find('input:hidden[id $= "-DELETE"]'),
+ buttonRow = row.siblings("a." + addCssSelector + ', .' + options.formCssClass + '-add'),
+ forms;
+ if (del.length) {
+ // We're dealing with an inline formset.
+ // Rather than remove this form from the DOM, we'll mark it as deleted
+ // and hide it, then let Django handle the deleting:
+ del.val('on');
+ row.hide();
+ forms = $('.' + options.formCssClass).not(':hidden');
+ } else {
+ row.remove();
+ // Update the TOTAL_FORMS count:
+ forms = $('.' + options.formCssClass).not('.formset-custom-template');
+ totalForms.val(forms.length);
+ }
+ for (var i=0, formCount=forms.length; i ');
+ row.hide();
+ } else {
+ del.before(' ');
+ }
+ // Hide any labels associated with the DELETE checkbox:
+ $('label[for="' + del.attr('id') + '"]').hide();
+ del.remove();
+ }
+ if (hasChildElements(row)) {
+ row.addClass(options.formCssClass);
+ if (row.is(':visible')) {
+ insertDeleteLink(row);
+ applyExtraClasses(row, i);
+ }
+ }
+ });
+
+ if ($$.length) {
+ var hideAddButton = !showAddButton(),
+ addButton, template;
+ if (options.formTemplate) {
+ // If a form template was specified, we'll clone it to generate new form instances:
+ template = (options.formTemplate instanceof $) ? options.formTemplate : $(options.formTemplate);
+ template.removeAttr('id').addClass(options.formCssClass + ' formset-custom-template');
+ template.find(childElementSelector).each(function() {
+ updateElementIndex($(this), options.prefix, '__prefix__');
+ });
+ insertDeleteLink(template);
+ } else {
+ // Otherwise, use the last form in the formset; this works much better if you've got
+ // extra (>= 1) forms (thnaks to justhamade for pointing this out):
+ template = $('.' + options.formCssClass + ':last').clone(true).removeAttr('id');
+ template.find('input:hidden[id $= "-DELETE"]').remove();
+ // Clear all cloned fields, except those the user wants to keep (thanks to brunogola for the suggestion):
+ template.find(childElementSelector).not(options.keepFieldValues).each(function() {
+ var elem = $(this);
+ // If this is a checkbox or radiobutton, uncheck it.
+ // This fixes Issue 1, reported by Wilson.Andrew.J:
+ if (elem.is('input:checkbox') || elem.is('input:radio')) {
+ elem.attr('checked', false);
+ } else {
+ elem.val('');
+ }
+ });
+ }
+ // FIXME: Perhaps using $.data would be a better idea?
+ options.formTemplate = template;
+
+ if ($$.is('TR')) {
+ // If forms are laid out as table rows, insert the
+ // "add" button in a new table row:
+ var numCols = $$.eq(0).children().length, // This is a bit of an assumption :|
+ buttonRow = $('' + options.addText + ' ')
+ .addClass(options.formCssClass + '-add');
+ $$.parent().append(buttonRow);
+ if (hideAddButton) buttonRow.hide();
+ addButton = buttonRow.find('a');
+ } else {
+ // Otherwise, insert it immediately after the last form:
+ $$.filter(':last').after('' + options.addText + ' ');
+ addButton = $$.filter(':last').next();
+ if (hideAddButton) addButton.hide();
+ }
+ addButton.click(function() {
+ var formCount = parseInt(totalForms.val()),
+ row = options.formTemplate.clone(true).removeClass('formset-custom-template'),
+ buttonRow = $($(this).parents('tr.' + options.formCssClass + '-add').get(0) || this),
+ delCssSelector = $.trim(options.deleteCssClass).replace(/\s+/g, '.');
+ applyExtraClasses(row, formCount);
+ row.insertBefore(buttonRow).show();
+ row.find(childElementSelector).each(function() {
+ updateElementIndex($(this), options.prefix, formCount);
+ });
+ totalForms.val(formCount + 1);
+ // Check if we're above the minimum allowed number of forms -> show all delete link(s)
+ if (showDeleteLinks()){
+ $('a.' + delCssSelector).each(function(){$(this).show();});
+ }
+ // Check if we've exceeded the maximum allowed number of forms:
+ if (!showAddButton()) buttonRow.hide();
+ // If a post-add callback was supplied, call it with the added form:
+ if (options.added) options.added(row);
+ return false;
+ });
+ }
+
+ return $$;
+ };
+
+ /* Setup plugin defaults */
+ $.fn.formset.defaults = {
+ prefix: 'form', // The form prefix for your django formset
+ formTemplate: null, // The jQuery selection cloned to generate new form instances
+ addText: 'add another', // Text for the add link
+ deleteText: 'remove', // Text for the delete link
+ addCssClass: 'add-row', // CSS class applied to the add link
+ deleteCssClass: 'delete-row', // CSS class applied to the delete link
+ formCssClass: 'dynamic-form', // CSS class applied to each form in a formset
+ extraClasses: [], // Additional CSS classes, which will be applied to each form in turn
+ keepFieldValues: '', // jQuery selector for fields whose values should be kept when the form is cloned
+ added: null, // Function called each time a new form is added
+ removed: null // Function called each time a form is deleted
+ };
+})(jQuery);
diff --git a/src/kasu/static/less/common.less b/src/kasu/static/less/common.less
index 0ad352f..b542bc6 100644
--- a/src/kasu/static/less/common.less
+++ b/src/kasu/static/less/common.less
@@ -163,7 +163,7 @@ ul {
list-style: circle outside;
padding-left: 2em;
margin-top:0.5em;
-
+
li {margin-bottom: 0.5em}
}
ul.info {
@@ -258,6 +258,7 @@ ul.tabs {
padding: 0;
}
.error, ul.errorlist li {color: #a40000;}
+input.error {border-color:#a40000; background-color: rgba(164, 0, 0, 0.25);}
.game h2 {margin: 0.5em 0;}
.grid_1,.grid_2,.grid_3,.grid_4,.grid_5,.grid_6,.grid_7,.grid_8,.grid_9,.grid_10,.grid_11,.grid_12
{
diff --git a/src/mahjong_ranking/forms.py b/src/mahjong_ranking/forms.py
index 4d15cad..4cca680 100644
--- a/src/mahjong_ranking/forms.py
+++ b/src/mahjong_ranking/forms.py
@@ -5,12 +5,12 @@ Created on 04.10.2011
@author: christian
"""
-from django.contrib.auth import get_user_model
from django import forms
+from django.contrib.auth import get_user_model
from django.utils.translation import ugettext as _
-from . import models
from events.models import Event
+from . import models
USER_MODEL = get_user_model()
@@ -48,6 +48,17 @@ class HanchanForm(forms.ModelForm):
self.fields[player_input_score].widget.attrs['type'] = 'number'
self.fields[player].queryset = player_queryset
+ def is_valid(self):
+ ret = forms.Form.is_valid(self)
+ for field, errors in self.errors.items():
+ message = ", ".join(set(errors))
+ print(type(field), type(errors))
+ self.fields[field].widget.attrs.update({
+ 'class': self.fields[field].widget.attrs.get('class', '') + ' error',
+ 'title': message
+ })
+ return ret
+
class HanchanAdminForm(HanchanForm):
""" Extends the HanchanForm for users with admin privileges.
@@ -60,6 +71,7 @@ class HanchanAdminForm(HanchanForm):
model = models.Hanchan
fields = HanchanForm.Meta.fields + ('confirmed',)
+
HanchanFormset = forms.inlineformset_factory(Event, models.Hanchan,
form=HanchanForm,
extra=1,
diff --git a/src/mahjong_ranking/locale/de/LC_MESSAGES/django.po b/src/mahjong_ranking/locale/de/LC_MESSAGES/django.po
index 6ca0ee0..ee79ef1 100644
--- a/src/mahjong_ranking/locale/de/LC_MESSAGES/django.po
+++ b/src/mahjong_ranking/locale/de/LC_MESSAGES/django.po
@@ -7,9 +7,9 @@ msgid ""
msgstr ""
"Project-Id-Version: kasu.mahjong_ranking\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-06-19 22:46+0200\n"
-"PO-Revision-Date: 2016-09-28 00:24+0200\n"
-"Last-Translator: Christian Berg \n"
+"POT-Creation-Date: 2018-01-11 22:50+0100\n"
+"PO-Revision-Date: 2018-01-12 15:23+0105\n"
+"Last-Translator: b'Christian Berg '\n"
"Language-Team: Kasu \n"
"Language: de\n"
"MIME-Version: 1.0\n"
@@ -17,381 +17,390 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 1.8.9\n"
-"X-Translated-Using: django-rosetta 0.7.6\n"
+"X-Translated-Using: django-rosetta 0.7.14\n"
-#: mahjong_ranking/admin.py:26
+#: src/mahjong_ranking/admin.py:24
msgid "Recalculate"
msgstr "Neuberechnen"
-#: mahjong_ranking/admin.py:36
+#: src/mahjong_ranking/admin.py:34
msgid "Confirm"
msgstr "Bestätigen"
-#: mahjong_ranking/admin.py:46
+#: src/mahjong_ranking/admin.py:44
msgid "Set unconfirmed"
msgstr "Als unbestätigt markieren"
-#: mahjong_ranking/forms.py:21
+#: src/mahjong_ranking/forms.py:22
msgid "start"
msgstr "Beginn"
-#: mahjong_ranking/models.py:89
-#: mahjong_ranking/templates/mahjong_ranking/player_dan_score.html:14
-#: mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html:13
-#: mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html:15
-#: mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html:15
-#: mahjong_ranking/templates/mahjong_ranking/seasonranking_list.html:10
+#: src/mahjong_ranking/models.py:91
+#: src/mahjong_ranking/templates/mahjong_ranking/player_dan_score.html:14
+#: src/mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html:13
+#: src/mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html:15
+#: src/mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html:15
+#: src/mahjong_ranking/templates/mahjong_ranking/seasonranking_list.html:10
msgid "Start"
msgstr "Beginn"
-#: mahjong_ranking/models.py:90
+#: src/mahjong_ranking/models.py:92
msgid "This is crucial to get the right Hanchans that scores"
msgstr "Wichtig damit die richtigen Hanchans in die Wertung kommen."
-#: mahjong_ranking/models.py:97
+#: src/mahjong_ranking/models.py:99
msgid "Player 1"
msgstr "Spieler 1"
-#: mahjong_ranking/models.py:98 mahjong_ranking/models.py:100
-#: mahjong_ranking/models.py:117 mahjong_ranking/models.py:119
-#: mahjong_ranking/models.py:136 mahjong_ranking/models.py:138
-#: mahjong_ranking/models.py:155 mahjong_ranking/models.py:157
-#: mahjong_ranking/templates/mahjong_ranking/eventhanchan_list.html:19
-#: mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:21
-#: mahjong_ranking/templates/mahjong_ranking/hanchan_confirm_delete.html:16
-#: mahjong_ranking/templates/mahjong_ranking/hanchan_form.html:19
-#: mahjong_ranking/templates/mahjong_ranking/kyudanranking_list.html:35
-#: mahjong_ranking/templates/mahjong_ranking/seasonranking_list.html:32
+#: src/mahjong_ranking/models.py:100 src/mahjong_ranking/models.py:102
+#: src/mahjong_ranking/models.py:119 src/mahjong_ranking/models.py:121
+#: src/mahjong_ranking/models.py:138 src/mahjong_ranking/models.py:140
+#: src/mahjong_ranking/models.py:157 src/mahjong_ranking/models.py:159
+#: src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_list.html:19
+#: src/mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:21
+#: src/mahjong_ranking/templates/mahjong_ranking/hanchan_confirm_delete.html:16
+#: src/mahjong_ranking/templates/mahjong_ranking/hanchan_form.html:19
+#: src/mahjong_ranking/templates/mahjong_ranking/kyudanranking_list.html:35
+#: src/mahjong_ranking/templates/mahjong_ranking/seasonranking_list.html:32
msgid "Score"
msgstr "Punkte"
-#: mahjong_ranking/models.py:110 mahjong_ranking/models.py:129
-#: mahjong_ranking/models.py:148 mahjong_ranking/models.py:167
-#: mahjong_ranking/models.py:169
-#: mahjong_ranking/templates/mahjong_ranking/hanchan_form.html:20
-#: mahjong_ranking/templates/mahjong_ranking/player_dan_score.html:18
-#: mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html:17
+#: src/mahjong_ranking/models.py:112 src/mahjong_ranking/models.py:131
+#: src/mahjong_ranking/models.py:150 src/mahjong_ranking/models.py:169
+#: src/mahjong_ranking/models.py:171
+#: src/mahjong_ranking/templates/mahjong_ranking/hanchan_form.html:20
+#: src/mahjong_ranking/templates/mahjong_ranking/player_dan_score.html:18
+#: src/mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html:17
msgid "Comment"
msgstr "Kommentar"
-#: mahjong_ranking/models.py:116
+#: src/mahjong_ranking/models.py:118
msgid "Player 2"
msgstr "Spieler 2"
-#: mahjong_ranking/models.py:135
+#: src/mahjong_ranking/models.py:137
msgid "Player 3"
msgstr "Spieler 3"
-#: mahjong_ranking/models.py:154
+#: src/mahjong_ranking/models.py:156
msgid "Player 4"
msgstr "Spieler 4"
-#: mahjong_ranking/models.py:170
+#: src/mahjong_ranking/models.py:173
msgid "Has been Confirmed"
msgstr "Wurde bestätigt"
-#: mahjong_ranking/models.py:172
+#: src/mahjong_ranking/models.py:174
msgid "Only valid and confirmed Hanchans will be counted in the rating."
msgstr "Nur gültige und bestätigte Hanchans kommen in die Wertung."
-#: mahjong_ranking/models.py:177 mahjong_ranking/models.py:576
-#: mahjong_ranking/templates/mahjong_ranking/ladder_redbox.html:29
-#: mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html:63
+#: src/mahjong_ranking/models.py:179 src/mahjong_ranking/models.py:607
+#: src/mahjong_ranking/templates/mahjong_ranking/ladder_redbox.html:29
+#: src/mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html:63
msgid "Season"
msgstr "Saison"
-#: mahjong_ranking/models.py:182
+#: src/mahjong_ranking/models.py:184
msgid "Hanchan"
msgstr "Hanchan"
-#: mahjong_ranking/models.py:183
-#: mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:17
+#: src/mahjong_ranking/models.py:185
+#: src/mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:17
msgid "Hanchans"
msgstr "Hanchans"
-#: mahjong_ranking/models.py:186
+#: src/mahjong_ranking/models.py:188
msgid "Hanchan from {0:%Y-%m-%d} at {0:%H:%M} with {1}"
msgstr "Hanchan vom {0:%Y-%m-%d} um {0:%H:%M} mit {1}"
-#: mahjong_ranking/models.py:213
+#: src/mahjong_ranking/models.py:215
#, python-format
msgid "%s can't attend the same game multiple times"
msgstr "%s kann an einem Spiel nicht mehrfach teilnehmen."
-#: mahjong_ranking/models.py:221
+#: src/mahjong_ranking/models.py:223
msgid "Games in the future may not be added, Dr. Brown"
msgstr "Spiele aus der Zukunft dürfen noch nicht erfasst werden. Dr. Brown."
-#: mahjong_ranking/models.py:223
+#: src/mahjong_ranking/models.py:225
msgid "Only games during the event are allowed"
msgstr "Nur Spiele während der Veranstaltung zählen."
-#: mahjong_ranking/models.py:226
+#: src/mahjong_ranking/models.py:228
msgid "Gamescore is lower then 100.000 Pt."
msgstr "Spielstand ist weniger als 100.000 Punkte"
-#: mahjong_ranking/models.py:228
+#: src/mahjong_ranking/models.py:230
msgid "Gamescore is over 100.000 Pt."
msgstr "Spielstand ist über 100.000 Punkte."
-#: mahjong_ranking/models.py:352
+#: src/mahjong_ranking/models.py:362
msgid "Kyū/Dan Ranking"
msgstr "Kyū/Dan Wertung"
-#: mahjong_ranking/models.py:353
+#: src/mahjong_ranking/models.py:363
msgid "Kyū/Dan Rankings"
msgstr "Kyū/Dan Wertungen"
-#: mahjong_ranking/templates/mahjong_ranking/eventhanchan_list.html:7
-msgid "Played Hanchans"
-msgstr "Gespielte Hanchans"
+#: src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_form.html:11
+#: src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_list.html:55
+#| msgid "Edit Hanchan"
+msgid "Edit Hanchans"
+msgstr "Hanchans bearbeiten"
-#: mahjong_ranking/templates/mahjong_ranking/eventhanchan_list.html:18
-#: mahjong_ranking/templates/mahjong_ranking/hanchan_confirm_delete.html:15
-msgid "Place"
-msgstr "Platz"
-
-#: mahjong_ranking/templates/mahjong_ranking/eventhanchan_list.html:21
-#: mahjong_ranking/templates/mahjong_ranking/hanchan_confirm_delete.html:18
-#: mahjong_ranking/templates/mahjong_ranking/player_dan_score.html:17
-msgid "Dan Points"
-msgstr "Dan Punkte"
-
-#: mahjong_ranking/templates/mahjong_ranking/eventhanchan_list.html:23
-#: mahjong_ranking/templates/mahjong_ranking/hanchan_confirm_delete.html:20
-#: mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html:16
-#: mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html:18
-msgid "Kyu Points"
-msgstr "Kyu Punkte"
-
-#: mahjong_ranking/templates/mahjong_ranking/eventhanchan_list.html:37
-#: mahjong_ranking/templates/mahjong_ranking/hanchan_confirm_delete.html:4
-#: mahjong_ranking/templates/mahjong_ranking/hanchan_confirm_delete.html:33
-#: mahjong_ranking/templates/mahjong_ranking/player_dan_score.html:44
-#: mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html:33
-#: mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html:41
-#: mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html:52
-msgid "Delete Hanchan"
-msgstr "Hanchan löschen"
-
-#: mahjong_ranking/templates/mahjong_ranking/eventhanchan_list.html:43
-#: mahjong_ranking/templates/mahjong_ranking/hanchan_form.html:4
-#: mahjong_ranking/templates/mahjong_ranking/hanchan_form.html:14
-#: mahjong_ranking/templates/mahjong_ranking/player_dan_score.html:47
-#: mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html:36
-#: mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html:44
-#: mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html:55
-msgid "Edit Hanchan"
-msgstr "Hanchan bearbeiten"
-
-#: mahjong_ranking/templates/mahjong_ranking/eventhanchan_list.html:48
-msgid "No Hanchan has been added to this event yet."
-msgstr "Für diese Veranstaltung wurde noch keine Hanchan eingetragen."
-
-#: mahjong_ranking/templates/mahjong_ranking/eventhanchan_list.html:54
-#: mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:51
-msgid "Edit Event"
-msgstr "Veranstaltung bearbeiten"
-
-#: mahjong_ranking/templates/mahjong_ranking/eventhanchan_list.html:55
-#: mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:52
-#: mahjong_ranking/templates/mahjong_ranking/hanchan_form.html:4
-#: mahjong_ranking/templates/mahjong_ranking/hanchan_form.html:14
+#: src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_form.html:82
+#: src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_list.html:56
+#: src/mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:52
+#: src/mahjong_ranking/templates/mahjong_ranking/hanchan_form.html:4
+#: src/mahjong_ranking/templates/mahjong_ranking/hanchan_form.html:14
msgid "Add Hanchan"
msgstr "Hanchan hinzufügen"
-#: mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:4
-#: mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:5
-msgid "Tournament Ranking"
-msgstr "Turnierwertung"
+#: src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_form.html:84
+#: src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_list.html:37
+#: src/mahjong_ranking/templates/mahjong_ranking/hanchan_confirm_delete.html:4
+#: src/mahjong_ranking/templates/mahjong_ranking/hanchan_confirm_delete.html:33
+#: src/mahjong_ranking/templates/mahjong_ranking/player_dan_score.html:44
+#: src/mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html:33
+#: src/mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html:41
+#: src/mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html:52
+msgid "Delete Hanchan"
+msgstr "Hanchan löschen"
-#: mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:12
-#: mahjong_ranking/templates/mahjong_ranking/kyudanranking_list.html:30
-#: mahjong_ranking/templates/mahjong_ranking/seasonranking_list.html:23
-msgid "Rank"
-msgstr "Rang"
-
-#: mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:13
-#: mahjong_ranking/templates/mahjong_ranking/kyudanranking_list.html:17
-#: mahjong_ranking/templates/mahjong_ranking/seasonranking_list.html:24
-msgid "Avatar"
-msgstr "Avatar"
-
-#: mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:14
-#: mahjong_ranking/templates/mahjong_ranking/kyudanranking_list.html:20
-#: mahjong_ranking/templates/mahjong_ranking/seasonranking_list.html:25
-msgid "Nickname"
-msgstr "Spitzname"
-
-#: mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:15
-#: mahjong_ranking/templates/mahjong_ranking/seasonranking_list.html:26
-msgid "Name"
-msgstr "Name"
-
-#: mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:16
-#: mahjong_ranking/templates/mahjong_ranking/seasonranking_list.html:27
-msgid "Average"
-msgstr "Durchschnitt"
-
-#: mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:20
-#: mahjong_ranking/templates/mahjong_ranking/player_dan_score.html:15
-#: mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html:15
-#: mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html:16
-#: mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html:16
-#: mahjong_ranking/templates/mahjong_ranking/seasonranking_list.html:31
-msgid "Placement"
-msgstr "Platzierung"
-
-#: mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:22
-#: mahjong_ranking/templates/mahjong_ranking/seasonranking_list.html:33
-msgid "count"
-msgstr "Anzahl"
-
-#: mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:23
-#: mahjong_ranking/templates/mahjong_ranking/seasonranking_list.html:34
-msgid "good"
-msgstr "gut"
-
-#: mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:24
-#: mahjong_ranking/templates/mahjong_ranking/seasonranking_list.html:35
-msgid "won"
-msgstr "gewonnen"
-
-#: mahjong_ranking/templates/mahjong_ranking/hanchan_confirm_delete.html:39
-msgid "Cancel"
-msgstr "Abbruch"
-
-#: mahjong_ranking/templates/mahjong_ranking/hanchan_confirm_delete.html:40
-msgid "Delete"
-msgstr "Löschen"
-
-#: mahjong_ranking/templates/mahjong_ranking/hanchan_form.html:18
-msgid "Player"
-msgstr "Spieler"
-
-#: mahjong_ranking/templates/mahjong_ranking/hanchan_form.html:58
-msgid "Total"
-msgstr "Total"
-
-#: mahjong_ranking/templates/mahjong_ranking/hanchan_form.html:71
+#: src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_form.html:94
+#: src/mahjong_ranking/templates/mahjong_ranking/hanchan_form.html:71
msgid "back"
msgstr "Zurück"
-#: mahjong_ranking/templates/mahjong_ranking/hanchan_form.html:72
+#: src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_form.html:95
+#: src/mahjong_ranking/templates/mahjong_ranking/hanchan_form.html:72
msgid "save"
msgstr "Speichern"
-#: mahjong_ranking/templates/mahjong_ranking/kyudanranking_list.html:4
-#: mahjong_ranking/templates/mahjong_ranking/kyudanranking_list.html:9
+#: src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_list.html:7
+msgid "Played Hanchans"
+msgstr "Gespielte Hanchans"
+
+#: src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_list.html:18
+#: src/mahjong_ranking/templates/mahjong_ranking/hanchan_confirm_delete.html:15
+msgid "Place"
+msgstr "Platz"
+
+#: src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_list.html:21
+#: src/mahjong_ranking/templates/mahjong_ranking/hanchan_confirm_delete.html:18
+#: src/mahjong_ranking/templates/mahjong_ranking/player_dan_score.html:17
+msgid "Dan Points"
+msgstr "Dan Punkte"
+
+#: src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_list.html:23
+#: src/mahjong_ranking/templates/mahjong_ranking/hanchan_confirm_delete.html:20
+#: src/mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html:16
+#: src/mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html:18
+msgid "Kyu Points"
+msgstr "Kyu Punkte"
+
+#: src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_list.html:43
+#: src/mahjong_ranking/templates/mahjong_ranking/hanchan_form.html:4
+#: src/mahjong_ranking/templates/mahjong_ranking/hanchan_form.html:14
+#: src/mahjong_ranking/templates/mahjong_ranking/player_dan_score.html:47
+#: src/mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html:36
+#: src/mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html:44
+#: src/mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html:55
+msgid "Edit Hanchan"
+msgstr "Hanchan bearbeiten"
+
+#: src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_list.html:48
+msgid "No Hanchan has been added to this event yet."
+msgstr "Für diese Veranstaltung wurde noch keine Hanchan eingetragen."
+
+#: src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_list.html:54
+#: src/mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:51
+msgid "Edit Event"
+msgstr "Veranstaltung bearbeiten"
+
+#: src/mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:4
+#: src/mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:5
+msgid "Tournament Ranking"
+msgstr "Turnierwertung"
+
+#: src/mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:12
+#: src/mahjong_ranking/templates/mahjong_ranking/kyudanranking_list.html:30
+#: src/mahjong_ranking/templates/mahjong_ranking/seasonranking_list.html:23
+msgid "Rank"
+msgstr "Rang"
+
+#: src/mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:13
+#: src/mahjong_ranking/templates/mahjong_ranking/kyudanranking_list.html:17
+#: src/mahjong_ranking/templates/mahjong_ranking/seasonranking_list.html:24
+msgid "Avatar"
+msgstr "Avatar"
+
+#: src/mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:14
+#: src/mahjong_ranking/templates/mahjong_ranking/kyudanranking_list.html:20
+#: src/mahjong_ranking/templates/mahjong_ranking/seasonranking_list.html:25
+msgid "Nickname"
+msgstr "Spitzname"
+
+#: src/mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:15
+#: src/mahjong_ranking/templates/mahjong_ranking/seasonranking_list.html:26
+msgid "Name"
+msgstr "Name"
+
+#: src/mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:16
+#: src/mahjong_ranking/templates/mahjong_ranking/seasonranking_list.html:27
+msgid "Average"
+msgstr "Durchschnitt"
+
+#: src/mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:20
+#: src/mahjong_ranking/templates/mahjong_ranking/player_dan_score.html:15
+#: src/mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html:15
+#: src/mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html:16
+#: src/mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html:16
+#: src/mahjong_ranking/templates/mahjong_ranking/seasonranking_list.html:31
+msgid "Placement"
+msgstr "Platzierung"
+
+#: src/mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:22
+#: src/mahjong_ranking/templates/mahjong_ranking/seasonranking_list.html:33
+msgid "count"
+msgstr "Anzahl"
+
+#: src/mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:23
+#: src/mahjong_ranking/templates/mahjong_ranking/seasonranking_list.html:34
+msgid "good"
+msgstr "gut"
+
+#: src/mahjong_ranking/templates/mahjong_ranking/eventranking_list.html:24
+#: src/mahjong_ranking/templates/mahjong_ranking/seasonranking_list.html:35
+msgid "won"
+msgstr "gewonnen"
+
+#: src/mahjong_ranking/templates/mahjong_ranking/hanchan_confirm_delete.html:39
+msgid "Cancel"
+msgstr "Abbruch"
+
+#: src/mahjong_ranking/templates/mahjong_ranking/hanchan_confirm_delete.html:40
+msgid "Delete"
+msgstr "Löschen"
+
+#: src/mahjong_ranking/templates/mahjong_ranking/hanchan_form.html:18
+msgid "Player"
+msgstr "Spieler"
+
+#: src/mahjong_ranking/templates/mahjong_ranking/hanchan_form.html:58
+msgid "Total"
+msgstr "Total"
+
+#: src/mahjong_ranking/templates/mahjong_ranking/kyudanranking_list.html:4
+#: src/mahjong_ranking/templates/mahjong_ranking/kyudanranking_list.html:9
msgid "Player List"
msgstr "Spieler Liste"
-#: mahjong_ranking/templates/mahjong_ranking/kyudanranking_list.html:25
+#: src/mahjong_ranking/templates/mahjong_ranking/kyudanranking_list.html:25
msgid "Full Name"
msgstr "Voller Name"
-#: mahjong_ranking/templates/mahjong_ranking/kyudanranking_list.html:40
+#: src/mahjong_ranking/templates/mahjong_ranking/kyudanranking_list.html:40
msgid "Games Total"
msgstr "Spiele total"
-#: mahjong_ranking/templates/mahjong_ranking/ladder_redbox.html:3
+#: src/mahjong_ranking/templates/mahjong_ranking/ladder_redbox.html:3
msgid "Latest Hanchans"
msgstr "Letzten Hanchans"
-#: mahjong_ranking/templates/mahjong_ranking/ladder_redbox.html:15
+#: src/mahjong_ranking/templates/mahjong_ranking/ladder_redbox.html:15
msgid "Latest Events"
msgstr "Letzte Veranstaltungen"
-#: mahjong_ranking/templates/mahjong_ranking/ladder_redbox.html:27
+#: src/mahjong_ranking/templates/mahjong_ranking/ladder_redbox.html:27
msgid "Ladder Archive"
msgstr "Ladder Archiv"
-#: mahjong_ranking/templates/mahjong_ranking/player_dan_score.html:4
-#: mahjong_ranking/templates/mahjong_ranking/player_dan_score.html:5
+#: src/mahjong_ranking/templates/mahjong_ranking/player_dan_score.html:4
+#: src/mahjong_ranking/templates/mahjong_ranking/player_dan_score.html:5
msgid "Dan Score for"
msgstr "Dan Wertung für"
-#: mahjong_ranking/templates/mahjong_ranking/player_dan_score.html:8
+#: src/mahjong_ranking/templates/mahjong_ranking/player_dan_score.html:8
msgid "Hanchans that apply to the Dan Score"
msgstr "Hanchans welche zur Dan Wertung zählen"
-#: mahjong_ranking/templates/mahjong_ranking/player_dan_score.html:12
-#: mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html:13
-#: mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html:13
+#: src/mahjong_ranking/templates/mahjong_ranking/player_dan_score.html:12
+#: src/mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html:13
+#: src/mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html:13
msgid "Date"
msgstr "Datum"
-#: mahjong_ranking/templates/mahjong_ranking/player_dan_score.html:13
-#: mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html:12
-#: mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html:14
-#: mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html:14
+#: src/mahjong_ranking/templates/mahjong_ranking/player_dan_score.html:13
+#: src/mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html:12
+#: src/mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html:14
+#: src/mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html:14
msgid "Event"
msgstr "Veranstaltung"
-#: mahjong_ranking/templates/mahjong_ranking/player_dan_score.html:16
-#: mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html:14
-#: mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html:17
-#: mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html:17
+#: src/mahjong_ranking/templates/mahjong_ranking/player_dan_score.html:16
+#: src/mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html:14
+#: src/mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html:17
+#: src/mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html:17
msgid "Players"
msgstr "Spieler"
-#: mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html:4
-#: mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html:6
+#: src/mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html:4
+#: src/mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html:6
msgid "Unconfirmed Hanchans from"
msgstr "Nicht bestätigte Hanchans von"
-#: mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html:9
+#: src/mahjong_ranking/templates/mahjong_ranking/player_invalid_score.html:9
msgid "Invalid hanchans with"
msgstr "Ungültige Hanchans mit"
-#: mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html:4
-#: mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html:6
+#: src/mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html:4
+#: src/mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html:6
msgid "Kyu Score for"
msgstr "Kyu Wertung für"
-#: mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html:9
+#: src/mahjong_ranking/templates/mahjong_ranking/player_kyu_score.html:9
msgid "Hanchans that apply to the Kyu Score"
msgstr "Hanchans welche zur Kyu Wertung zählen"
-#: mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html:4
-#: mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html:5
+#: src/mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html:4
+#: src/mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html:5
msgid "Ladder Score for"
msgstr "Ladder Wertung für"
-#: mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html:8
+#: src/mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html:8
msgid "Hanchans that apply to the Ladder Score"
msgstr "Hanchans welche in der Ladder zählen"
-#: mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html:71
+#: src/mahjong_ranking/templates/mahjong_ranking/player_ladder_score.html:71
msgid "Go"
msgstr "Los"
-#: mahjong_ranking/templates/mahjong_ranking/seasonranking_list.html:11
+#: src/mahjong_ranking/templates/mahjong_ranking/seasonranking_list.html:11
msgid "End"
msgstr "Ende"
-#: mahjong_ranking/templates/mahjong_ranking/seasonranking_list.html:12
+#: src/mahjong_ranking/templates/mahjong_ranking/seasonranking_list.html:12
msgid "Participants"
msgstr "Teilnehmer"
-#: mahjong_ranking/views.py:98
+#: src/mahjong_ranking/views.py:104
#, python-format
msgid "%s has been updated successfully."
msgstr "%s wurde erfolgreich aktualisiert."
-#: mahjong_ranking/views.py:101
+#: src/mahjong_ranking/views.py:107
#, python-format
msgid "%s has been added successfully. You can now add a new one."
msgstr "%s wurde erfolgreich hinzugefügt. Du kannst eine neue eintragen."
-#: mahjong_ranking/views.py:119 mahjong_ranking/views.py:135
-msgid "Event does not exist"
-msgstr "Veranstaltung existiert nicht"
-
-#: mahjong_ranking/views.py:199
+#: src/mahjong_ranking/views.py:207
msgid "No user found matching the name {}"
msgstr "Kein Benutzer mit dem Namen %s gefunden"
+#~ msgid "Event does not exist"
+#~ msgstr "Veranstaltung existiert nicht"
+
#~ msgid "It's not allowed to enter future games."
#~ msgstr "Spiele in der Zukunft sind nicht erlaubt."
diff --git a/src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_form.html b/src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_form.html
index de744d9..1ae7d4f 100755
--- a/src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_form.html
+++ b/src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_form.html
@@ -2,9 +2,15 @@
{% block title %}Hanchans: {{ event.name }}{% endblock %}
-{% block maincontent %}{% trans 'Edit Hanchans' %}
+{% block extra_head %}
+
+
+{% endblock %}
-
+
{% endblock %}
-{% block comments %}{% endblock %}{% block buttonbar %}{% endblock %}
+{% block comments %}{% endblock %}
+
+{% block buttonbar %}
+ {% trans 'back' %}
+ {% trans 'save' %}
+{% endblock %}
diff --git a/src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_list.html b/src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_list.html
index 941c5e3..ce36ab5 100755
--- a/src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_list.html
+++ b/src/mahjong_ranking/templates/mahjong_ranking/eventhanchan_list.html
@@ -52,6 +52,7 @@
{% block buttonbar %}
{% if perms.mahjong_ranking.add_hanchan %}
{% trans 'Edit Event' %}
+ {% trans 'Edit Hanchans' %}
{% trans 'Add Hanchan' %}
{% endif %}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/src/mahjong_ranking/views.py b/src/mahjong_ranking/views.py
index 1c305b1..4be1877 100644
--- a/src/mahjong_ranking/views.py
+++ b/src/mahjong_ranking/views.py
@@ -111,7 +111,7 @@ class HanchanForm(SuccessMessageMixin, EventDetailMixin,
class EventHanchanForm(EventDetailMixin, PermissionRequiredMixin,
generic.TemplateView):
"""Display a Formset to add and Edit Hanchans of the specific Event."""
- permission_required = 'mahjong_ranking.edit_hanchan'
+ permission_required = 'mahjong_ranking.add_hanchan'
template_name = 'mahjong_ranking/eventhanchan_form.html'
model=models.Hanchan
diff --git a/src/maistar_ranking/locale/de/LC_MESSAGES/django.po b/src/maistar_ranking/locale/de/LC_MESSAGES/django.po
index 05fe21b..67ae8c4 100644
--- a/src/maistar_ranking/locale/de/LC_MESSAGES/django.po
+++ b/src/maistar_ranking/locale/de/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: kasu.mahjong_ranking\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-06-19 22:46+0200\n"
+"POT-Creation-Date: 2018-01-11 22:50+0100\n"
"PO-Revision-Date: 2016-09-28 00:24+0200\n"
"Last-Translator: Christian Berg \n"
"Language-Team: Kasu \n"
@@ -19,208 +19,208 @@ msgstr ""
"X-Generator: Poedit 1.8.9\n"
"X-Translated-Using: django-rosetta 0.7.2\n"
-#: maistar_ranking/admin.py:19
+#: src/maistar_ranking/admin.py:24
msgid "Recalculate"
msgstr "Neuberechnen"
-#: maistar_ranking/forms.py:33
+#: src/maistar_ranking/forms.py:35
#, python-format
msgid "%s may only participate once."
msgstr "%s darf nur einmal teilnehmen."
-#: maistar_ranking/models.py:20
+#: src/maistar_ranking/models.py:21
msgid "Comment"
msgstr "Kommentar"
-#: maistar_ranking/models.py:22
+#: src/maistar_ranking/models.py:24
msgid "Player 1"
msgstr "Spieler 1"
-#: maistar_ranking/models.py:24 maistar_ranking/models.py:30
-#: maistar_ranking/models.py:36 maistar_ranking/models.py:42
-#: maistar_ranking/models.py:48 maistar_ranking/models.py:54
-#: maistar_ranking/templates/maistar_ranking/ranking_list.html:19
+#: src/maistar_ranking/models.py:26 src/maistar_ranking/models.py:33
+#: src/maistar_ranking/models.py:40 src/maistar_ranking/models.py:47
+#: src/maistar_ranking/models.py:54 src/maistar_ranking/models.py:61
+#: src/maistar_ranking/templates/maistar_ranking/ranking_list.html:19
msgid "Score"
msgstr "Punkte"
-#: maistar_ranking/models.py:28
+#: src/maistar_ranking/models.py:31
msgid "Player 2"
msgstr "Spieler 2"
-#: maistar_ranking/models.py:34
+#: src/maistar_ranking/models.py:38
msgid "Player 3"
msgstr "Spieler 3"
-#: maistar_ranking/models.py:40
+#: src/maistar_ranking/models.py:45
msgid "Player 4"
msgstr "Spieler 4"
-#: maistar_ranking/models.py:46
+#: src/maistar_ranking/models.py:52
msgid "Player 5"
msgstr "Spieler 5"
-#: maistar_ranking/models.py:52
+#: src/maistar_ranking/models.py:59
msgid "Player 6"
msgstr "Spieler 6"
-#: maistar_ranking/models.py:58
+#: src/maistar_ranking/models.py:65
msgid "Has been confirmed"
msgstr "Wurde bestätigt"
-#: maistar_ranking/models.py:60
+#: src/maistar_ranking/models.py:67
msgid "the game only counts whe it has been confirmed"
msgstr "das Spiel zählt nur wenn es bestätigt wurde"
-#: maistar_ranking/models.py:63 maistar_ranking/models.py:148
-#: maistar_ranking/templates/maistar_ranking/player_game_list.html:6
-#: maistar_ranking/templates/maistar_ranking/ranking_list.html:4
-#: maistar_ranking/templates/maistar_ranking/ranking_list.html:72
+#: src/maistar_ranking/models.py:70 src/maistar_ranking/models.py:153
+#: src/maistar_ranking/templates/maistar_ranking/player_game_list.html:6
+#: src/maistar_ranking/templates/maistar_ranking/ranking_list.html:4
+#: src/maistar_ranking/templates/maistar_ranking/ranking_list.html:72
msgid "Season"
msgstr "Saison"
-#: maistar_ranking/models.py:69
+#: src/maistar_ranking/models.py:80
msgid "Mai-Star Game with {0} from {1:%Y-%m-%d}"
msgstr "Mai-Star Spiel mit {0} vom {1:%Y-%m-%d}"
-#: maistar_ranking/templates/maistar_ranking/game_form.html:5
-#: maistar_ranking/templates/maistar_ranking/game_form.html:16
-#: maistar_ranking/templates/maistar_ranking/game_list.html:27
-#: maistar_ranking/templates/maistar_ranking/player_game_list.html:44
+#: src/maistar_ranking/templates/maistar_ranking/game_form.html:5
+#: src/maistar_ranking/templates/maistar_ranking/game_form.html:16
+#: src/maistar_ranking/templates/maistar_ranking/game_list.html:27
+#: src/maistar_ranking/templates/maistar_ranking/player_game_list.html:44
msgid "Edit Game"
msgstr "Spiel bearbeiten"
-#: maistar_ranking/templates/maistar_ranking/game_form.html:5
-#: maistar_ranking/templates/maistar_ranking/game_form.html:16
-#: maistar_ranking/templates/maistar_ranking/game_list.html:41
+#: src/maistar_ranking/templates/maistar_ranking/game_form.html:5
+#: src/maistar_ranking/templates/maistar_ranking/game_form.html:16
+#: src/maistar_ranking/templates/maistar_ranking/game_list.html:41
msgid "Add Game"
msgstr "Spiel hinzufügen"
-#: maistar_ranking/templates/maistar_ranking/game_form.html:76
+#: src/maistar_ranking/templates/maistar_ranking/game_form.html:76
msgid "Back"
msgstr "Zurück"
-#: maistar_ranking/templates/maistar_ranking/game_form.html:77
+#: src/maistar_ranking/templates/maistar_ranking/game_form.html:77
msgid "Save"
msgstr "Speichern"
-#: maistar_ranking/templates/maistar_ranking/game_list.html:4
-#: maistar_ranking/templates/maistar_ranking/player_game_list.html:6
+#: src/maistar_ranking/templates/maistar_ranking/game_list.html:4
+#: src/maistar_ranking/templates/maistar_ranking/player_game_list.html:6
msgid "Mai-Star Games"
msgstr "Mai-Star Spiele"
-#: maistar_ranking/templates/maistar_ranking/game_list.html:7
+#: src/maistar_ranking/templates/maistar_ranking/game_list.html:7
msgid "Played Mai-Star Games"
msgstr "Gespielte Mai-Star Spiele"
-#: maistar_ranking/templates/maistar_ranking/game_list.html:11
+#: src/maistar_ranking/templates/maistar_ranking/game_list.html:11
msgid "Game"
msgstr "Spiel"
-#: maistar_ranking/templates/maistar_ranking/game_list.html:14
+#: src/maistar_ranking/templates/maistar_ranking/game_list.html:14
msgid "Place"
msgstr "Platz"
-#: maistar_ranking/templates/maistar_ranking/game_list.html:19
-#: maistar_ranking/templates/maistar_ranking/player_game_list.html:36
+#: src/maistar_ranking/templates/maistar_ranking/game_list.html:19
+#: src/maistar_ranking/templates/maistar_ranking/player_game_list.html:36
msgid "Points"
msgstr "Punkte"
-#: maistar_ranking/templates/maistar_ranking/game_list.html:24
-#: maistar_ranking/templates/maistar_ranking/player_game_list.html:41
+#: src/maistar_ranking/templates/maistar_ranking/game_list.html:24
+#: src/maistar_ranking/templates/maistar_ranking/player_game_list.html:41
msgid "Delete Game"
msgstr "Spiel löschen"
-#: maistar_ranking/templates/maistar_ranking/game_list.html:33
+#: src/maistar_ranking/templates/maistar_ranking/game_list.html:33
msgid "No Mai-Star games have been added to this event yet."
msgstr "Für diese Veranstaltung wurden noch keine Mai-Star Spiele erfasst."
-#: maistar_ranking/templates/maistar_ranking/game_list.html:40
+#: src/maistar_ranking/templates/maistar_ranking/game_list.html:40
msgid "Edit Event"
msgstr "Veranstaltung bearbeiten"
-#: maistar_ranking/templates/maistar_ranking/hanchan_confirm_delete.html:4
-#: maistar_ranking/templates/maistar_ranking/hanchan_confirm_delete.html:10
+#: src/maistar_ranking/templates/maistar_ranking/hanchan_confirm_delete.html:4
+#: src/maistar_ranking/templates/maistar_ranking/hanchan_confirm_delete.html:10
msgid "Delete game"
msgstr "Spiel löschen"
-#: maistar_ranking/templates/maistar_ranking/hanchan_confirm_delete.html:13
+#: src/maistar_ranking/templates/maistar_ranking/hanchan_confirm_delete.html:13
msgid "Cancel"
msgstr "Abbrechen"
-#: maistar_ranking/templates/maistar_ranking/hanchan_confirm_delete.html:14
+#: src/maistar_ranking/templates/maistar_ranking/hanchan_confirm_delete.html:14
msgid "Delete"
msgstr "Löschen"
-#: maistar_ranking/templates/maistar_ranking/page.html:5
+#: src/maistar_ranking/templates/maistar_ranking/page.html:5
msgid "Archive"
msgstr "Archiv"
-#: maistar_ranking/templates/maistar_ranking/page.html:7
+#: src/maistar_ranking/templates/maistar_ranking/page.html:7
msgid "Add Event"
msgstr "Veranstaltung hinzufügen"
-#: maistar_ranking/templates/maistar_ranking/player_game_list.html:4
+#: src/maistar_ranking/templates/maistar_ranking/player_game_list.html:4
msgid "Ladder Score for"
msgstr "Ladder Wertung für"
-#: maistar_ranking/templates/maistar_ranking/player_game_list.html:9
+#: src/maistar_ranking/templates/maistar_ranking/player_game_list.html:9
msgid "Mai-Star Games with"
msgstr "Mai-Star Spiele mit"
-#: maistar_ranking/templates/maistar_ranking/player_game_list.html:14
+#: src/maistar_ranking/templates/maistar_ranking/player_game_list.html:14
msgid "Date"
msgstr "Datum"
-#: maistar_ranking/templates/maistar_ranking/player_game_list.html:15
+#: src/maistar_ranking/templates/maistar_ranking/player_game_list.html:15
msgid "Event"
msgstr "Veranstaltung"
-#: maistar_ranking/templates/maistar_ranking/player_game_list.html:16
+#: src/maistar_ranking/templates/maistar_ranking/player_game_list.html:16
msgid "Players"
msgstr "Spieler"
-#: maistar_ranking/templates/maistar_ranking/ranking_list.html:4
+#: src/maistar_ranking/templates/maistar_ranking/ranking_list.html:4
msgid "Mai-Star Ranking"
msgstr "Mai-Star Platzierung"
-#: maistar_ranking/templates/maistar_ranking/ranking_list.html:10
-#: maistar_ranking/templates/maistar_ranking/ranking_list.html:18
+#: src/maistar_ranking/templates/maistar_ranking/ranking_list.html:10
+#: src/maistar_ranking/templates/maistar_ranking/ranking_list.html:18
msgid "Placement"
msgstr "Platzierung"
-#: maistar_ranking/templates/maistar_ranking/ranking_list.html:11
+#: src/maistar_ranking/templates/maistar_ranking/ranking_list.html:11
msgid "Avatar"
msgstr "Avatar"
-#: maistar_ranking/templates/maistar_ranking/ranking_list.html:12
+#: src/maistar_ranking/templates/maistar_ranking/ranking_list.html:12
msgid "Nickname"
msgstr "Spitzname"
-#: maistar_ranking/templates/maistar_ranking/ranking_list.html:13
+#: src/maistar_ranking/templates/maistar_ranking/ranking_list.html:13
msgid "Name"
msgstr "Name"
-#: maistar_ranking/templates/maistar_ranking/ranking_list.html:14
+#: src/maistar_ranking/templates/maistar_ranking/ranking_list.html:14
msgid "Average"
msgstr "Durchschnitt"
-#: maistar_ranking/templates/maistar_ranking/ranking_list.html:15
+#: src/maistar_ranking/templates/maistar_ranking/ranking_list.html:15
msgid "Games"
msgstr "Spiele"
-#: maistar_ranking/templates/maistar_ranking/ranking_list.html:20
+#: src/maistar_ranking/templates/maistar_ranking/ranking_list.html:20
msgid "count"
msgstr "Anzahl"
-#: maistar_ranking/templates/maistar_ranking/ranking_list.html:21
+#: src/maistar_ranking/templates/maistar_ranking/ranking_list.html:21
msgid "good"
msgstr "Gut"
-#: maistar_ranking/templates/maistar_ranking/ranking_list.html:22
+#: src/maistar_ranking/templates/maistar_ranking/ranking_list.html:22
msgid "won"
msgstr "Gewonnen"
-#: maistar_ranking/templates/maistar_ranking/ranking_list.html:43
+#: src/maistar_ranking/templates/maistar_ranking/ranking_list.html:43
msgid ""
"Unfortunately, nobody has it been done in the ranking.\n"
" A player must have 6 games done, to be added to the ranking."
@@ -229,15 +229,15 @@ msgstr ""
"als 6 Spiele innerhalb einer Saison absolviert haben, werden für das "
"Endergebnis nicht gewertet."
-#: maistar_ranking/templates/maistar_ranking/ranking_list.html:52
+#: src/maistar_ranking/templates/maistar_ranking/ranking_list.html:52
msgid "Latest Games"
msgstr "Letzten Spiele"
-#: maistar_ranking/templates/maistar_ranking/ranking_list.html:63
+#: src/maistar_ranking/templates/maistar_ranking/ranking_list.html:63
msgid "Latest Events"
msgstr "Letzten Veranstaltungen"
-#: maistar_ranking/templates/maistar_ranking/ranking_list.html:70
+#: src/maistar_ranking/templates/maistar_ranking/ranking_list.html:70
msgid "Ladder Archive"
msgstr "Archiv"
diff --git a/src/membership/locale/de/LC_MESSAGES/django.po b/src/membership/locale/de/LC_MESSAGES/django.po
index f1a9257..de7d6c6 100644
--- a/src/membership/locale/de/LC_MESSAGES/django.po
+++ b/src/membership/locale/de/LC_MESSAGES/django.po
@@ -7,196 +7,195 @@ msgid ""
msgstr ""
"Project-Id-Version: kasu.membership\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-06-19 22:46+0200\n"
-"PO-Revision-Date: 2016-09-28 00:24+0200\n"
-"Last-Translator: Christian Berg \n"
+"POT-Creation-Date: 2018-01-11 22:50+0100\n"
+"PO-Revision-Date: 2018-01-12 15:22+0105\n"
+"Last-Translator: b'Christian Berg '\n"
"Language-Team: Kasu \n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-"X-Translated-Using: django-rosetta 0.7.2\n"
"X-Generator: Poedit 1.8.9\n"
+"X-Translated-Using: django-rosetta 0.7.14\n"
-#: membership/admin.py:22
+#: src/membership/__init__.py:11
+msgid "Male"
+msgstr "Männlich"
+
+#: src/membership/__init__.py:12
+msgid "Female"
+msgstr "Weiblich"
+
+#: src/membership/admin.py:22
msgid "Activate selected User"
msgstr "Ausgewählte Benutzer freischalten"
-#: membership/admin.py:31
+#: src/membership/admin.py:38
msgid "Cleanup selected Activation Requests"
msgstr "Ausgewählte Aktivierungsanfragen bereinigen"
-#: membership/admin.py:37
+#: src/membership/admin.py:47
msgid "Group"
msgstr "Gruppe"
-#: membership/admin.py:38
+#: src/membership/admin.py:48
msgid "Groups"
msgstr "Gruppen"
-#: membership/admin.py:59 membership/models.py:164 membership/models.py:215
-#: membership/templates/membership/register_form.html:32
+#: src/membership/admin.py:72 src/membership/models.py:163
+#: src/membership/models.py:216
+#: src/membership/templates/membership/register_form.html:32
msgid "Membership"
msgstr "Mitgliedschaft"
-#: membership/admin.py:64
+#: src/membership/admin.py:77
msgid "Permissions"
msgstr "Berechtigung"
-#: membership/admin.py:66
+#: src/membership/admin.py:79
msgid "Important dates"
msgstr "Wichtige Daten"
-#: membership/forms.py:23
+#: src/membership/forms.py:23
msgid "birthday"
msgstr "Geburtstag"
-#: membership/forms.py:25
+#: src/membership/forms.py:25
msgid "Input format: yyyy-mm-dd"
msgstr "Eingabeformat: tt.mm.jjjj"
-#: membership/forms.py:27
+#: src/membership/forms.py:27
msgid "Email"
msgstr "E-Mail"
-#: membership/forms.py:39 membership/forms.py:46 membership/forms.py:53
+#: src/membership/forms.py:42 src/membership/forms.py:50
+#: src/membership/forms.py:58
msgid ""
"For your membership, we need this. Please fill out this field "
"yet."
msgstr "Diese Angabe wird für eine Mitgliedschaft benötigt, bitte ausfüllen."
-#: membership/forms.py:59
+#: src/membership/forms.py:65
msgid ""
"For your membership, we need this. Please fill out this field "
"yet."
msgstr "Diese Angabe wird für eine Mitgliedschaft benötigt, bitte ausfüllen."
-#: membership/forms.py:72
+#: src/membership/forms.py:78
msgid "password"
msgstr "Passwort"
-#: membership/forms.py:74
+#: src/membership/forms.py:80
msgid "password (again)"
msgstr "Passwort (wiederholen)"
-#: membership/forms.py:97
+#: src/membership/forms.py:102
msgid "This username is already taken. Please choose another."
-msgstr "Diesen Benutzername ist schon vergeben. Bitte einen anderen auswählen."
+msgstr ""
+"Diesen Benutzername ist schon vergeben. Bitte einen anderen auswählen."
-#: membership/forms.py:106
+#: src/membership/forms.py:109
msgid ""
"This email address is already in use. Please supply a different "
"email address."
msgstr "Die E-Mail Adresse wird schon verwendet. Bitte eine andere angeben."
-#: membership/forms.py:115
+#: src/membership/forms.py:119
msgid "The two password fields didn't match."
msgstr "Die beiden Passwörter passen nicht."
-#: membership/models.py:20
-msgid "Male"
-msgstr "Männlich"
-
-#: membership/models.py:21
-msgid "Female"
-msgstr "Weiblich"
-
-#: membership/models.py:90
+#: src/membership/models.py:84
msgid "user"
msgstr "Benutzer"
-#: membership/models.py:92
+#: src/membership/models.py:86
msgid "activation key"
msgstr "Aktivierungsschlüssel"
-#: membership/models.py:96
+#: src/membership/models.py:90
msgid "pending activation"
msgstr "Ausstehende Aktivierung"
-#: membership/models.py:97
+#: src/membership/models.py:91
msgid "pending activations"
msgstr "Wartende Aktivierungen"
-#: membership/models.py:100
+#: src/membership/models.py:94
#, python-format
msgid "user registration for %s"
msgstr "Benutzerregistrierung für %s"
-#: membership/models.py:147
+#: src/membership/models.py:149
msgid "Gender"
msgstr "Geschlecht"
-#: membership/models.py:166
+#: src/membership/models.py:165
msgid ""
"Yes, I confirm that I am in agreement with the statutes and would "
"like to become a member."
msgstr "Ja, ich bin mit den Statuen einverstanden und möchte Mitglied werden."
-#: membership/models.py:170
+#: src/membership/models.py:169
msgid "Birthday Date"
msgstr "Geburtstag"
-#: membership/models.py:174
+#: src/membership/models.py:173
msgid "Telephone"
msgstr "Telefon"
-#: membership/models.py:180
+#: src/membership/models.py:179
msgid "Address"
msgstr "Adresse"
-#: membership/models.py:186
+#: src/membership/models.py:185
msgid "Postcode"
msgstr "Postleitzahl"
-#: membership/models.py:191
+#: src/membership/models.py:190
msgid "Town/City"
msgstr "Ort"
-#: membership/models.py:199
+#: src/membership/models.py:198
msgid "Paid until"
msgstr "Bezahlt bis"
-#: membership/models.py:205
+#: src/membership/models.py:204
msgid "Confirmed"
msgstr "Bestätigt"
-#: membership/models.py:207
+#: src/membership/models.py:206
msgid "This person has paid the membership fee."
msgstr "Diese Person hat ihre Mitgliedschaft bezahlt"
-#: membership/models.py:216
+#: src/membership/models.py:217
msgid "Memberships"
msgstr "Mitgliedschaften"
-#: membership/templates/membership/email/activation_email.txt:2
+#: src/membership/templates/membership/email/activation_email.txt:2
#, python-format
msgid "Welcome %(user)s,"
msgstr "Herzlich willkommen %(user)s,"
-#: membership/templates/membership/email/activation_email.txt:4
+#: src/membership/templates/membership/email/activation_email.txt:4
#, python-format
msgid ""
"We received an account request on %(site.domain)s for your email address.\n"
"To activate your account please visit the following link:"
msgstr ""
-"Jemand (hoffentlich du selbst) möchte mit dieser E-Mail Adresse einen neuen "
-"Benutzer Account für %(site.domain)s anlegen.\n"
-"Solltest du diesen Account aktivieren wollen, klicke bitte auf den unten "
-"stehenden Link, oder kopiere diesen in die Adresszeile deines Browsers:"
+"Jemand (hoffentlich du selbst) möchte mit dieser E-Mail Adresse einen neuen Benutzer Account für %(site.domain)s anlegen.\n"
+"Solltest du diesen Account aktivieren wollen, klicke bitte auf den unten stehenden Link, oder kopiere diesen in die Adresszeile deines Browsers:"
-#: membership/templates/membership/email/activation_email.txt:9
+#: src/membership/templates/membership/email/activation_email.txt:9
#, python-format
msgid ""
-"If you do not want to open an account on %(site.domain)s, please ignore this "
-"email.\n"
+"If you do not want to open an account on %(site.domain)s, please ignore this email.\n"
"Your information will then be deleted in a few days time."
msgstr ""
-"Wenn du keinen Zugang für %(site.domain)s eröffnen willst, ignoriere diese E-"
-"Mail bitte.\n"
+"Wenn du keinen Zugang für %(site.domain)s eröffnen willst, ignoriere diese E-Mail bitte.\n"
"Die Zugangsdaten werden dann in ein paar Tagen automatisch gelöscht."
-#: membership/templates/membership/email/activation_email.txt:12
+#: src/membership/templates/membership/email/activation_email.txt:12
#, python-format
msgid ""
"Best Regards,\n"
@@ -205,216 +204,215 @@ msgstr ""
"mit den besten Wünschen\n"
"Das Team von %(site.domain)s"
-#: membership/templates/membership/email/password_reset_email.html:2
+#: src/membership/templates/membership/email/password_reset_email.html:2
msgid "You're receiving this e-mail because you requested a password reset"
msgstr ""
"Du hast diese E-Mail erhalten, weil jemand die das Passwort zurücksetzen "
"möchte. "
-#: membership/templates/membership/email/password_reset_email.html:3
+#: src/membership/templates/membership/email/password_reset_email.html:3
#, python-format
msgid "for your user account at %(site_name)s"
msgstr "Für deinen Benutzerzugang auf %(site_name)s"
-#: membership/templates/membership/email/password_reset_email.html:5
+#: src/membership/templates/membership/email/password_reset_email.html:5
msgid "Please go to the following page and choose a new password:"
msgstr "Bitte gehe auf folgende Seite und wähle ein neues Passwort aus:"
-#: membership/templates/membership/email/password_reset_email.html:9
+#: src/membership/templates/membership/email/password_reset_email.html:9
msgid "Your username, in case you've forgotten:"
msgstr "Dein Benutzername, für den Fall das du diesen vergessen hast:"
-#: membership/templates/membership/email/password_reset_email.html:11
+#: src/membership/templates/membership/email/password_reset_email.html:11
msgid "Thanks for using our site!"
msgstr "Danke das du unsere Seite verwendest!"
-#: membership/templates/membership/email/password_reset_email.html:13
+#: src/membership/templates/membership/email/password_reset_email.html:13
#, python-format
msgid "The %(site_name)s team"
msgstr "Das %(site_name)s Team"
-#: membership/templates/membership/email/password_reset_subject.txt:2
+#: src/membership/templates/membership/email/password_reset_subject.txt:2
#, python-format
msgid "Password reset on %(site_name)s"
msgstr "Passwort auf %(site_name)s zurücksetzen"
-#: membership/templates/membership/hanchan_table.html:5
+#: src/membership/templates/membership/hanchan_table.html:5
msgid "Start"
msgstr "Beginn"
-#: membership/templates/membership/hanchan_table.html:6
+#: src/membership/templates/membership/hanchan_table.html:6
msgid "Event"
msgstr "Termin"
-#: membership/templates/membership/hanchan_table.html:7
+#: src/membership/templates/membership/hanchan_table.html:7
msgid "Players"
msgstr "Spieler"
-#: membership/templates/membership/hanchan_table.html:8
+#: src/membership/templates/membership/hanchan_table.html:8
msgid "Kyu Points"
msgstr "Kyū Punkte"
-#: membership/templates/membership/hanchan_table.html:9
+#: src/membership/templates/membership/hanchan_table.html:9
msgid "Dan Points"
msgstr "Dan Punkte"
-#: membership/templates/membership/hanchan_table.html:10
+#: src/membership/templates/membership/hanchan_table.html:10
msgid "Bonus Points"
msgstr "Bonus Punkte"
-#: membership/templates/membership/hanchan_table.html:11
+#: src/membership/templates/membership/hanchan_table.html:11
msgid "Comment"
msgstr "Anmerkung"
-#: membership/templates/membership/hanchan_table.html:26
+#: src/membership/templates/membership/hanchan_table.html:26
msgid "This Hanchan does not validate"
msgstr "Diese Hanchan ist ungültig"
-#: membership/templates/membership/membership_detail.html:6
+#: src/membership/templates/membership/membership_detail.html:6
msgid "profile for"
msgstr "Profil für"
-#: membership/templates/membership/membership_detail.html:10
+#: src/membership/templates/membership/membership_detail.html:10
msgid "Ladder Hanchans"
msgstr "Ladder Hanchans"
-#: membership/templates/membership/membership_detail.html:11
+#: src/membership/templates/membership/membership_detail.html:11
msgid "Kyu Hanchans"
msgstr "Kyū Hanchans"
-#: membership/templates/membership/membership_detail.html:12
+#: src/membership/templates/membership/membership_detail.html:12
msgid "Dan Hanchans"
msgstr "Dan Hanchans"
-#: membership/templates/membership/membership_detail.html:13
+#: src/membership/templates/membership/membership_detail.html:13
msgid "Invalid Hanchans"
msgstr "Ungültige Hanchans"
-#: membership/templates/membership/membership_detail.html:14
+#: src/membership/templates/membership/membership_detail.html:14
msgid "Mai-Star Games"
msgstr "Mai-Star Spiele"
-#: membership/templates/membership/membership_detail.html:20
+#: src/membership/templates/membership/membership_detail.html:20
msgid "Profile Image"
msgstr "Profilbild"
-#: membership/templates/membership/membership_detail.html:28
+#: src/membership/templates/membership/membership_detail.html:28
msgid "Member Since"
msgstr "Mitglied seit"
-#: membership/templates/membership/membership_detail.html:29
+#: src/membership/templates/membership/membership_detail.html:29
msgid "Last Login"
msgstr "Letzte Anmeldung"
-#: membership/templates/membership/membership_detail.html:38
-#: membership/templates/membership/membership_detail.html:40
+#: src/membership/templates/membership/membership_detail.html:39
+#: src/membership/templates/membership/membership_detail.html:43
msgid "Points"
msgstr "Punkte"
-#: membership/templates/membership/membership_detail.html:42
+#: src/membership/templates/membership/membership_detail.html:40
+msgid "Maximum"
+msgstr "Maximum"
+
+#: src/membership/templates/membership/membership_detail.html:45
msgid "Games Total"
msgstr "Spiele gesamt"
-#: membership/templates/membership/membership_detail.html:43
-#: membership/templates/membership/membership_detail.html:45
+#: src/membership/templates/membership/membership_detail.html:46
+#: src/membership/templates/membership/membership_detail.html:48
msgid "Won"
msgstr "Gewonnen"
-#: membership/templates/membership/membership_detail.html:43
-#: membership/templates/membership/membership_detail.html:45
+#: src/membership/templates/membership/membership_detail.html:46
+#: src/membership/templates/membership/membership_detail.html:48
msgid "Good"
msgstr "Gut"
-#: membership/templates/membership/membership_detail.html:45
+#: src/membership/templates/membership/membership_detail.html:48
msgid "Current Season"
msgstr "Aktuelle Saison"
-#: membership/templates/membership/membership_detail.html:55
+#: src/membership/templates/membership/membership_detail.html:58
msgid "Edit Profile"
msgstr "Profil bearbeiten"
-#: membership/templates/membership/membership_detail.html:59
-#: membership/templates/registration/password_change_form.html:23
+#: src/membership/templates/membership/membership_detail.html:62
+#: src/membership/templates/registration/password_change_form.html:23
msgid "Change Password"
msgstr "Passwort ändern"
-#: membership/templates/membership/membership_detail.html:63
-#: membership/templates/membership/membership_detail.html:67
-#: membership/templates/membership/membership_detail.html:71
+#: src/membership/templates/membership/membership_detail.html:66
+#: src/membership/templates/membership/membership_detail.html:70
+#: src/membership/templates/membership/membership_detail.html:74
#, python-format
msgid "Associate with %(name)s"
msgstr "Verbinde mit %(name)s"
-#: membership/templates/membership/membership_form.html:4
-#: membership/templates/membership/membership_form.html:6
-#: membership/templates/membership/membership_form.html:11
+#: src/membership/templates/membership/membership_form.html:4
+#: src/membership/templates/membership/membership_form.html:6
+#: src/membership/templates/membership/membership_form.html:11
msgid "Edit Userprofile"
msgstr "Profil bearbeiten"
-#: membership/templates/membership/membership_form.html:15
+#: src/membership/templates/membership/membership_form.html:15
msgid "Reset"
msgstr "Zurücksetzen"
-#: membership/templates/membership/membership_form.html:16
+#: src/membership/templates/membership/membership_form.html:16
msgid "Save"
msgstr "Speichern"
-#: membership/templates/membership/register_form.html:4
-#: membership/templates/membership/register_form.html:7
+#: src/membership/templates/membership/register_form.html:4
+#: src/membership/templates/membership/register_form.html:7
msgid "Registration"
msgstr "Registrieren"
-#: membership/templates/membership/register_form.html:9
+#: src/membership/templates/membership/register_form.html:9
msgid ""
"After you've provided your account data, you'll receive\n"
-" an email asking you to verify your email address. You have to click on "
-"the\n"
-" link in this verification email to confirm your email address, "
-"otherwise\n"
+" an email asking you to verify your email address. You have to click on the\n"
+" link in this verification email to confirm your email address, otherwise\n"
" your can't login."
msgstr ""
-"Nach dem du deine Daten eingegeben hast, wirst du eine E-Mail zur "
-"Bestätigung bekommen.\n"
-"Bitte klicke auf den Link in dieser E-Mail zur Verifizierung, erst dann ist "
-"die Anmeldung möglich."
+"Nach dem du deine Daten eingegeben hast, wirst du eine E-Mail zur Bestätigung bekommen.\n"
+"Bitte klicke auf den Link in dieser E-Mail zur Verifizierung, erst dann ist die Anmeldung möglich."
-#: membership/templates/membership/register_form.html:20
+#: src/membership/templates/membership/register_form.html:20
msgid "name"
msgstr "Name"
-#: membership/templates/membership/register_form.html:26
-#: membership/templates/registration/login.html:41
+#: src/membership/templates/membership/register_form.html:26
+#: src/membership/templates/registration/login.html:41
msgid "login"
msgstr "Anmelden"
-#: membership/templates/membership/register_form.html:39
+#: src/membership/templates/membership/register_form.html:39
msgid "reset"
msgstr "Zurücksetzen"
-#: membership/templates/membership/register_form.html:41
-#: membership/templates/registration/login.html:35
+#: src/membership/templates/membership/register_form.html:41
+#: src/membership/templates/registration/login.html:35
msgid "register"
msgstr "Registrieren"
-#: membership/templates/membership/register_successful.html:5
-#: membership/templates/membership/register_successful.html:7
-#: membership/templates/membership/register_successful.html:10
+#: src/membership/templates/membership/register_successful.html:5
+#: src/membership/templates/membership/register_successful.html:7
+#: src/membership/templates/membership/register_successful.html:10
msgid "Activation sent"
msgstr "Aktivierung wurde zugesendet"
-#: membership/templates/registration/login.html:4
-#: membership/templates/registration/login.html:11
-#: membership/templates/registration/login.html:53
-#: membership/templates/registration/password_reset_complete.html:13
+#: src/membership/templates/registration/login.html:4
+#: src/membership/templates/registration/login.html:11
+#: src/membership/templates/registration/login.html:53
+#: src/membership/templates/registration/password_reset_complete.html:13
msgid "Login"
msgstr "Anmelden"
-#: membership/templates/registration/login.html:17
+#: src/membership/templates/registration/login.html:17
msgid "Have you already registered?"
msgstr "Bereits registriert?"
-#: membership/templates/registration/login.html:18
-#, fuzzy
+#: src/membership/templates/registration/login.html:18
#| msgid ""
#| "\n"
#| "As a registered member you can:
\n"
@@ -441,17 +439,14 @@ msgstr ""
" Kommentare auf dieser Seite hinterlassen. \n"
" Dich für unseren Newsletter anmelden \n"
" Mitglied in unserem Verein werden \n"
-" Vereinsmitglieder haben auch vollen Zugang zu unserem Ranking System"
-"li>\n"
-" \n"
+" Vereinsmitglieder haben auch vollen Zugang zu unserem Ranking System \n"
+""
-#: membership/templates/registration/login.html:27
-#, fuzzy
+#: src/membership/templates/registration/login.html:27
#| msgid ""
#| "\n"
#| "You can register here with your Google, or Facebook account.\n"
-#| "If you don't own such an account, or do not want to use it for "
-#| "authentication,\n"
+#| "If you don't own such an account, or do not want to use it for authentication,\n"
#| "you can fill out our registration form.
\n"
msgid ""
"\n"
@@ -462,80 +457,79 @@ msgid ""
" "
msgstr ""
"\n"
-"Du kannst dich auch über deinen Facebook, Google, oder Twitter Account "
-"anmelden.\n"
+"
Du kannst dich auch über deinen Facebook, Google, oder Twitter Account anmelden.\n"
"Wenn du so etwas nicht besitzt, oder nicht verwenden möchtest, \n"
-"kannst du auch das Registrierungsformular ausfüllen.
\n"
+"kannst du auch das Registrierungsformular ausfüllen.
"
-#: membership/templates/registration/login.html:45
-#, fuzzy
+#: src/membership/templates/registration/login.html:45
#| msgid "Your username and password didn't match. Please try again."
msgid ""
"Your username and password didn't match. Please try\n"
" again."
msgstr ""
-"Benutzername und Passwort stimmen nicht überein. Bitte noch einmal versuchen."
+"Benutzername und Passwort stimmen nicht überein. Bitte noch einmal "
+"versuchen."
-#: membership/templates/registration/login.html:50
+#: src/membership/templates/registration/login.html:50
msgid "Forgot your Password?"
msgstr "Passwort vergessen?"
-#: membership/templates/registration/login.html:60
+#: src/membership/templates/registration/login.html:60
msgid "or login with an existing Account"
msgstr "oder über einen existierenden Account anmelden"
-#: membership/templates/registration/login.html:63
+#: src/membership/templates/registration/login.html:63
msgid "Login with Facebook"
msgstr "Über Facebook anmelden"
-#: membership/templates/registration/login.html:66
+#: src/membership/templates/registration/login.html:66
msgid "Login with Twitter"
msgstr "Über Twitter anmelden"
-#: membership/templates/registration/login.html:69
+#: src/membership/templates/registration/login.html:69
msgid "Login with Google"
msgstr "Über Google Anmelden"
-#: membership/templates/registration/password_change_done.html:4
-#: membership/templates/registration/password_change_done.html:7
+#: src/membership/templates/registration/password_change_done.html:4
+#: src/membership/templates/registration/password_change_done.html:7
msgid "Password change successful"
msgstr "Benutzerprofil erfolgreich geändert."
-#: membership/templates/registration/password_change_done.html:8
+#: src/membership/templates/registration/password_change_done.html:8
msgid "Your password was changed."
msgstr "Passwort geändet"
-#: membership/templates/registration/password_change_form.html:4
-#: membership/templates/registration/password_change_form.html:9
-#: membership/templates/registration/password_change_form.html:16
+#: src/membership/templates/registration/password_change_form.html:4
+#: src/membership/templates/registration/password_change_form.html:9
+#: src/membership/templates/registration/password_change_form.html:16
msgid "Password change"
msgstr "Passwort wechseln"
-#: membership/templates/registration/password_change_form.html:10
+#: src/membership/templates/registration/password_change_form.html:10
msgid ""
-"Please enter your old password, for security's sake, and then enter your new "
-"password twice so we can verify you typed it in correctly."
+"Please enter your old password, for security's sake, and then enter your new"
+" password twice so we can verify you typed it in correctly."
msgstr ""
-"Zur Sicherheit bitte altes Passwort einmal und das gewünschte neue Passwort "
-"zweimal angeben, so können Tippfehler abgefangen werden."
+"Zur Sicherheit bitte altes Passwort einmal und das gewünschte neue Passwort"
+" zweimal angeben, so können Tippfehler abgefangen werden."
-#: membership/templates/registration/password_reset_complete.html:4
-#: membership/templates/registration/password_reset_complete.html:6
-#: membership/templates/registration/password_reset_complete.html:9
+#: src/membership/templates/registration/password_reset_complete.html:4
+#: src/membership/templates/registration/password_reset_complete.html:6
+#: src/membership/templates/registration/password_reset_complete.html:9
msgid "Password reset complete"
msgstr "Das Rücksetzen des Passwortes ist abgeschlossen."
-#: membership/templates/registration/password_reset_complete.html:10
+#: src/membership/templates/registration/password_reset_complete.html:10
msgid "Your password has been set. You may go ahead and log in now."
msgstr "Das Passwort wurde gesetzt, Du kannst dich nun damit anmelden."
-#: membership/templates/registration/password_reset_confirm.html:4
-#: membership/templates/registration/password_reset_confirm.html:6
-#: membership/templates/registration/password_reset_confirm.html:15
+#: src/membership/templates/registration/password_reset_confirm.html:4
+#: src/membership/templates/registration/password_reset_confirm.html:6
+#: src/membership/templates/registration/password_reset_confirm.html:15
msgid "Enter new password"
msgstr "Neues Passwort eingeben"
-#: membership/templates/registration/password_reset_confirm.html:12
+#: src/membership/templates/registration/password_reset_confirm.html:12
msgid ""
"Please enter your new password twice so we can verify you typed it in "
"correctly."
@@ -543,15 +537,15 @@ msgstr ""
"Bitte das Passwort zweimal eingeben, um sicher zu stellen das es korrekt "
"eingetippt wurde."
-#: membership/templates/registration/password_reset_confirm.html:18
+#: src/membership/templates/registration/password_reset_confirm.html:18
msgid "Change my password"
msgstr "Passwort ändern"
-#: membership/templates/registration/password_reset_confirm.html:26
+#: src/membership/templates/registration/password_reset_confirm.html:26
msgid "Password reset unsuccessful"
msgstr "Passwort rücksetzen fehlgeschlagen"
-#: membership/templates/registration/password_reset_confirm.html:27
+#: src/membership/templates/registration/password_reset_confirm.html:27
msgid ""
"The password reset link was invalid, possibly because it has already been "
"used. Please request a new password reset."
@@ -559,22 +553,22 @@ msgstr ""
"Der Link für die Rücksetzung des Passwortes war ungültig, vermutlich wurde "
"er schon einmal benutzt. Bitte eine neue Rücksetzung beantragen."
-#: membership/templates/registration/password_reset_done.html:4
-#: membership/templates/registration/password_reset_done.html:6
-#: membership/templates/registration/password_reset_done.html:12
+#: src/membership/templates/registration/password_reset_done.html:4
+#: src/membership/templates/registration/password_reset_done.html:6
+#: src/membership/templates/registration/password_reset_done.html:12
msgid "Password reset successful"
msgstr "Passwort erfolgreich zurückgesetzt."
-#: membership/templates/registration/password_reset_form.html:4
-#: membership/templates/registration/password_reset_form.html:6
+#: src/membership/templates/registration/password_reset_form.html:4
+#: src/membership/templates/registration/password_reset_form.html:6
msgid "Password reset"
msgstr "Passwort zurücksetzen"
-#: membership/templates/registration/password_reset_form.html:21
+#: src/membership/templates/registration/password_reset_form.html:21
msgid "Transmit"
msgstr "Übermitteln"
-#: membership/views.py:63
+#: src/membership/views.py:61
msgid ""
"Activation successful. You can now login anytime with you username "
"and password."
@@ -582,15 +576,14 @@ msgstr ""
"Die Aktivierung war erfolgreich. Du kannst dich ab jetzt jederzeit mit "
"deinem Benutzernamen und Passwort anmelden."
-#: membership/views.py:83
+#: src/membership/views.py:88
msgid "User Profile changed successfully"
msgstr "Benutzerprofil erfolgreich geändert."
-#: membership/views.py:97
-#, fuzzy
+#: src/membership/views.py:112
#| msgid "No %(verbose_name)s found matching the query"
msgid "No Membership found matching the query"
-msgstr "Kein %(verbose_name)s gefunden welche der Anfrage entspricht"
+msgstr "Kein Mitglied gefunden welche der Anfrage entspricht"
#~ msgid "Given Name"
#~ msgstr "Vorname"
@@ -599,9 +592,9 @@ msgstr "Kein %(verbose_name)s gefunden welche der Anfrage entspricht"
#~ msgstr "Nachname"
#~ msgid ""
-#~ "The Username can only contain the letters from A to Z, Numbers, "
-#~ "and the underscore. It must be at least 2 characters long, and "
-#~ "cannot be longer the 30. The first character must be a letter."
+#~ "The Username can only contain the letters from A to Z, Numbers, and "
+#~ "the underscore. It must be at least 2 characters long, and cannot be"
+#~ " longer the 30. The first character must be a letter."
#~ msgstr ""
#~ "Der Benutzername kann aus den Buchstaben A-Z, Ziffern und dem Unterstrich "
#~ "bestehen. Es sollte wenigstens 2, aber maximal 30 Zeichen lang sein. Das "
diff --git a/src/utils/locale/de/LC_MESSAGES/django.po b/src/utils/locale/de/LC_MESSAGES/django.po
index 6bac1e3..3deb814 100644
--- a/src/utils/locale/de/LC_MESSAGES/django.po
+++ b/src/utils/locale/de/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: kasu.utils\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-06-19 22:46+0200\n"
+"POT-Creation-Date: 2018-01-11 22:50+0100\n"
"PO-Revision-Date: 2016-09-28 00:24+0200\n"
"Last-Translator: Christian Berg \n"
"Language-Team: Kasu \n"
@@ -18,999 +18,999 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 1.8.9\n"
-#: utils/__init__.py:17
+#: src/utils/__init__.py:17
msgid "Rejected"
msgstr "Zurückgewiesen"
-#: utils/__init__.py:18
+#: src/utils/__init__.py:18
msgid "Waiting..."
msgstr "Wartend..."
-#: utils/__init__.py:19
+#: src/utils/__init__.py:19
msgid "Published"
msgstr "Veröffentlicht"
-#: utils/countries.py:4
+#: src/utils/countries.py:5
msgid "United Kingdom"
msgstr "Vereinigtes Königreich"
-#: utils/countries.py:5
+#: src/utils/countries.py:6
msgid "Afghanistan"
msgstr "Afghanistan"
-#: utils/countries.py:6
+#: src/utils/countries.py:7
msgid "Aland Islands"
msgstr "Aland Islands"
-#: utils/countries.py:7
+#: src/utils/countries.py:8
msgid "Albania"
msgstr "Albanien"
-#: utils/countries.py:8
+#: src/utils/countries.py:9
msgid "Algeria"
msgstr "Algerien"
-#: utils/countries.py:9
+#: src/utils/countries.py:10
msgid "American Samoa"
msgstr "Amerikanisch-Samoa"
-#: utils/countries.py:10
+#: src/utils/countries.py:11
msgid "Andorra"
msgstr "Andorra"
-#: utils/countries.py:11
+#: src/utils/countries.py:12
msgid "Angola"
msgstr "Angola"
-#: utils/countries.py:12
+#: src/utils/countries.py:13
msgid "Anguilla"
msgstr "Anguilla"
-#: utils/countries.py:13
+#: src/utils/countries.py:14
msgid "Antarctica"
msgstr "Antarktika"
-#: utils/countries.py:14
+#: src/utils/countries.py:15
msgid "Antigua and Barbuda"
msgstr "Antigua und Barbuda"
-#: utils/countries.py:15
+#: src/utils/countries.py:16
msgid "Argentina"
msgstr "Argentinien"
-#: utils/countries.py:16
+#: src/utils/countries.py:17
msgid "Armenia"
msgstr "Armenien"
-#: utils/countries.py:17
+#: src/utils/countries.py:18
msgid "Aruba"
msgstr "Aruba"
-#: utils/countries.py:18
+#: src/utils/countries.py:19
msgid "Australia"
msgstr "Australien"
-#: utils/countries.py:19
+#: src/utils/countries.py:20
msgid "Austria"
msgstr "Österreich"
-#: utils/countries.py:20
+#: src/utils/countries.py:21
msgid "Azerbaijan"
msgstr "Aserbaidschan"
-#: utils/countries.py:21
+#: src/utils/countries.py:22
msgid "Bahamas"
msgstr "Bahamas"
-#: utils/countries.py:22
+#: src/utils/countries.py:23
msgid "Bahrain"
msgstr "Bahrein"
-#: utils/countries.py:23
+#: src/utils/countries.py:24
msgid "Bangladesh"
msgstr "Bangladesch"
-#: utils/countries.py:24
+#: src/utils/countries.py:25
msgid "Barbados"
msgstr "Barbados"
-#: utils/countries.py:25
+#: src/utils/countries.py:26
msgid "Belarus"
msgstr "Weißrussland"
-#: utils/countries.py:26
+#: src/utils/countries.py:27
msgid "Belgium"
msgstr "Belgien"
-#: utils/countries.py:27
+#: src/utils/countries.py:28
msgid "Belize"
msgstr "Belize"
-#: utils/countries.py:28
+#: src/utils/countries.py:29
msgid "Benin"
msgstr "Benin"
-#: utils/countries.py:29
+#: src/utils/countries.py:30
msgid "Bermuda"
msgstr "Bermuda"
-#: utils/countries.py:30
+#: src/utils/countries.py:31
msgid "Bhutan"
msgstr "Bhutan"
-#: utils/countries.py:31
+#: src/utils/countries.py:32
msgid "Bolivia"
msgstr "Bolivien"
-#: utils/countries.py:32
+#: src/utils/countries.py:33
msgid "Bosnia and Herzegovina"
msgstr "Bosnien und Herzegowina"
-#: utils/countries.py:33
+#: src/utils/countries.py:34
msgid "Botswana"
msgstr "Botswana"
-#: utils/countries.py:34
+#: src/utils/countries.py:35
msgid "Bouvet Island"
msgstr "Bouvet Island"
-#: utils/countries.py:35
+#: src/utils/countries.py:36
msgid "Brazil"
msgstr "Brasilien"
-#: utils/countries.py:36
+#: src/utils/countries.py:37
msgid "British Indian Ocean Territory"
msgstr "British Indian Ocean Territory"
-#: utils/countries.py:37
+#: src/utils/countries.py:38
msgid "Brunei Darussalam"
msgstr "Brunei Darussalam"
-#: utils/countries.py:38
+#: src/utils/countries.py:39
msgid "Bulgaria"
msgstr "Bulgarien"
-#: utils/countries.py:39
+#: src/utils/countries.py:40
msgid "Burkina Faso"
msgstr "Burkina Faso"
-#: utils/countries.py:40
+#: src/utils/countries.py:41
msgid "Burundi"
msgstr "Burundi"
-#: utils/countries.py:41
+#: src/utils/countries.py:42
msgid "Cambodia"
msgstr "Kambodscha"
-#: utils/countries.py:42
+#: src/utils/countries.py:43
msgid "Cameroon"
msgstr "Kamerun"
-#: utils/countries.py:43
+#: src/utils/countries.py:44
msgid "Canada"
msgstr "Kanada"
-#: utils/countries.py:44
+#: src/utils/countries.py:45
msgid "Cape Verde"
msgstr "Cape Verde"
-#: utils/countries.py:45
+#: src/utils/countries.py:46
msgid "Cayman Islands"
msgstr "Cayman Islands"
-#: utils/countries.py:46
+#: src/utils/countries.py:47
msgid "Central African Republic"
msgstr "Zentralafrikanische Republik"
-#: utils/countries.py:47
+#: src/utils/countries.py:48
msgid "Chad"
msgstr "Tschad"
-#: utils/countries.py:48
+#: src/utils/countries.py:49
msgid "Chile"
msgstr "Chile"
-#: utils/countries.py:49
+#: src/utils/countries.py:50
msgid "China"
msgstr "China"
-#: utils/countries.py:50
+#: src/utils/countries.py:51
msgid "Christmas Island"
msgstr "Christmas Island"
-#: utils/countries.py:51
+#: src/utils/countries.py:52
msgid "Cocos (Keeling) Islands"
msgstr "Cocos (Keeling) Islands"
-#: utils/countries.py:52
+#: src/utils/countries.py:53
msgid "Colombia"
msgstr "Kolumbien"
-#: utils/countries.py:53
+#: src/utils/countries.py:54
msgid "Comoros"
msgstr "Komoren"
-#: utils/countries.py:54
+#: src/utils/countries.py:55
msgid "Congo"
msgstr "Kongo"
-#: utils/countries.py:55
+#: src/utils/countries.py:56
msgid "Congo, The Democratic Republic of the"
msgstr "Kongo, Demokratische Republik"
-#: utils/countries.py:56
+#: src/utils/countries.py:57
msgid "Cook Islands"
msgstr "Cook-Inseln"
-#: utils/countries.py:57
+#: src/utils/countries.py:58
msgid "Costa Rica"
msgstr "Costa Rica"
-#: utils/countries.py:58
+#: src/utils/countries.py:59
msgid "Cote d'Ivoire"
msgstr "Cote d'Ivoire"
-#: utils/countries.py:59
+#: src/utils/countries.py:60
msgid "Croatia"
msgstr "Kroatien"
-#: utils/countries.py:60
+#: src/utils/countries.py:61
msgid "Cuba"
msgstr "Kuba"
-#: utils/countries.py:61
+#: src/utils/countries.py:62
msgid "Cyprus"
msgstr "Zypern"
-#: utils/countries.py:62
+#: src/utils/countries.py:63
msgid "Czech Republic"
msgstr "Tschechische Republik"
-#: utils/countries.py:63
+#: src/utils/countries.py:64
msgid "Denmark"
msgstr "Dänemark"
-#: utils/countries.py:64
+#: src/utils/countries.py:65
msgid "Djibouti"
msgstr "Dschibuti"
-#: utils/countries.py:65
+#: src/utils/countries.py:66
msgid "Dominica"
msgstr "Dominica"
-#: utils/countries.py:66
+#: src/utils/countries.py:67
msgid "Dominican Republic"
msgstr "Dominikanische Republik"
-#: utils/countries.py:67
+#: src/utils/countries.py:68
msgid "Ecuador"
msgstr "Ecuador"
-#: utils/countries.py:68
+#: src/utils/countries.py:69
msgid "Egypt"
msgstr "Ägypten"
-#: utils/countries.py:69
+#: src/utils/countries.py:70
msgid "El Salvador"
msgstr "El Salvador"
-#: utils/countries.py:70
+#: src/utils/countries.py:71
msgid "Equatorial Guinea"
msgstr "Äquatorial-Guinea"
-#: utils/countries.py:71
+#: src/utils/countries.py:72
msgid "Eritrea"
msgstr "Eritrea"
-#: utils/countries.py:72
+#: src/utils/countries.py:73
msgid "Estonia"
msgstr "Estland"
-#: utils/countries.py:73
+#: src/utils/countries.py:74
msgid "Ethiopia"
msgstr "Äthiopien"
-#: utils/countries.py:74
+#: src/utils/countries.py:75
msgid "Falkland Islands (Malvinas)"
msgstr "Falklandinseln (Malvinas)"
-#: utils/countries.py:75
+#: src/utils/countries.py:76
msgid "Faroe Islands"
msgstr "Färöer-Inseln"
-#: utils/countries.py:76
+#: src/utils/countries.py:77
msgid "Fiji"
msgstr "Fidschi"
-#: utils/countries.py:77
+#: src/utils/countries.py:78
msgid "Finland"
msgstr "Finnland"
-#: utils/countries.py:78
+#: src/utils/countries.py:79
msgid "France"
msgstr "Frankreich"
-#: utils/countries.py:79
+#: src/utils/countries.py:80
msgid "French Guiana"
msgstr "Französisch-Guayana"
-#: utils/countries.py:80
+#: src/utils/countries.py:81
msgid "French Polynesia"
msgstr "Französisch-Polynesien"
-#: utils/countries.py:81
+#: src/utils/countries.py:82
msgid "French Southern Territories"
msgstr "Französisch Südliche Territorien"
-#: utils/countries.py:82
+#: src/utils/countries.py:83
msgid "Gabon"
msgstr "Gabun"
-#: utils/countries.py:83
+#: src/utils/countries.py:84
msgid "Gambia"
msgstr "Gambia"
-#: utils/countries.py:84
+#: src/utils/countries.py:85
msgid "Georgia"
msgstr "Georgia"
-#: utils/countries.py:85
+#: src/utils/countries.py:86
msgid "Germany"
msgstr "Deutschland"
-#: utils/countries.py:86
+#: src/utils/countries.py:87
msgid "Ghana"
msgstr "Ghana"
-#: utils/countries.py:87
+#: src/utils/countries.py:88
msgid "Gibraltar"
msgstr "Gibraltar"
-#: utils/countries.py:88
+#: src/utils/countries.py:89
msgid "Greece"
msgstr "Griechenland"
-#: utils/countries.py:89
+#: src/utils/countries.py:90
msgid "Greenland"
msgstr "Grönland"
-#: utils/countries.py:90
+#: src/utils/countries.py:91
msgid "Grenada"
msgstr "Grenada"
-#: utils/countries.py:91
+#: src/utils/countries.py:92
msgid "Guadeloupe"
msgstr "Guadeloupe"
-#: utils/countries.py:92
+#: src/utils/countries.py:93
msgid "Guam"
msgstr "Guam"
-#: utils/countries.py:93
+#: src/utils/countries.py:94
msgid "Guatemala"
msgstr "Guatemala"
-#: utils/countries.py:94
+#: src/utils/countries.py:95
msgid "Guernsey"
msgstr "Guernsey"
-#: utils/countries.py:95
+#: src/utils/countries.py:96
msgid "Guinea"
msgstr "Guinea"
-#: utils/countries.py:96
+#: src/utils/countries.py:97
msgid "Guinea-Bissau"
msgstr "Guinea-Bissau"
-#: utils/countries.py:97
+#: src/utils/countries.py:98
msgid "Guyana"
msgstr "Guyana"
-#: utils/countries.py:98
+#: src/utils/countries.py:99
msgid "Haiti"
msgstr "Haiti"
-#: utils/countries.py:99
+#: src/utils/countries.py:100
msgid "Heard Island and McDonald Islands"
msgstr "Heard und McDonald Inseln"
-#: utils/countries.py:100
+#: src/utils/countries.py:101
msgid "Holy See (Vatican City State)"
msgstr "Heiliger Stuhl (Vatikanstadt)"
-#: utils/countries.py:101
+#: src/utils/countries.py:102
msgid "Honduras"
msgstr "Honduras"
-#: utils/countries.py:102
+#: src/utils/countries.py:103
msgid "Hong Kong"
msgstr "Hongkong"
-#: utils/countries.py:103
+#: src/utils/countries.py:104
msgid "Hungary"
msgstr "Ungarn"
-#: utils/countries.py:104
+#: src/utils/countries.py:105
msgid "Iceland"
msgstr "Island"
-#: utils/countries.py:105
+#: src/utils/countries.py:106
msgid "India"
msgstr "Indien"
-#: utils/countries.py:106
+#: src/utils/countries.py:107
msgid "Indonesia"
msgstr "Indonesien"
-#: utils/countries.py:107
+#: src/utils/countries.py:108
msgid "Iran, Islamic Republic of"
msgstr "Iran, Islamische Republik"
-#: utils/countries.py:108
+#: src/utils/countries.py:109
msgid "Iraq"
msgstr "Irak"
-#: utils/countries.py:109
+#: src/utils/countries.py:110
msgid "Ireland"
msgstr "Irland"
-#: utils/countries.py:110
+#: src/utils/countries.py:111
msgid "Isle of Man"
msgstr "Isle of Man"
-#: utils/countries.py:111
+#: src/utils/countries.py:112
msgid "Israel"
msgstr "Israel"
-#: utils/countries.py:112
+#: src/utils/countries.py:113
msgid "Italy"
msgstr "Italien"
-#: utils/countries.py:113
+#: src/utils/countries.py:114
msgid "Jamaica"
msgstr "Jamaika"
-#: utils/countries.py:114
+#: src/utils/countries.py:115
msgid "Japan"
msgstr "Japan"
-#: utils/countries.py:115
+#: src/utils/countries.py:116
msgid "Jersey"
msgstr "Jersey"
-#: utils/countries.py:116
+#: src/utils/countries.py:117
msgid "Jordan"
msgstr "Jordan"
-#: utils/countries.py:117
+#: src/utils/countries.py:118
msgid "Kazakhstan"
msgstr "Kasachstan"
-#: utils/countries.py:118
+#: src/utils/countries.py:119
msgid "Kenya"
msgstr "Kenia"
-#: utils/countries.py:119
+#: src/utils/countries.py:120
msgid "Kiribati"
msgstr "Kiribati"
-#: utils/countries.py:120
+#: src/utils/countries.py:121
msgid "Korea, Democratic People's Republic of"
msgstr "Korea, Demokratische Volksrepublik"
-#: utils/countries.py:121
+#: src/utils/countries.py:122
msgid "Korea, Republic of"
msgstr "Korea, Republik"
-#: utils/countries.py:122
+#: src/utils/countries.py:123
msgid "Kuwait"
msgstr "Kuwait"
-#: utils/countries.py:123
+#: src/utils/countries.py:124
msgid "Kyrgyzstan"
msgstr "Kirgisistan"
-#: utils/countries.py:124
+#: src/utils/countries.py:125
msgid "Lao People's Democratic Republic"
msgstr "Lao Demokratischen Volksrepublik"
-#: utils/countries.py:125
+#: src/utils/countries.py:126
msgid "Latvia"
msgstr "Lettland"
-#: utils/countries.py:126
+#: src/utils/countries.py:127
msgid "Lebanon"
msgstr "Libanon"
-#: utils/countries.py:127
+#: src/utils/countries.py:128
msgid "Lesotho"
msgstr "Lesotho"
-#: utils/countries.py:128
+#: src/utils/countries.py:129
msgid "Liberia"
msgstr "Liberia"
-#: utils/countries.py:129
+#: src/utils/countries.py:130
msgid "Libyan Arab Jamahiriya"
msgstr "Libyen"
-#: utils/countries.py:130
+#: src/utils/countries.py:131
msgid "Liechtenstein"
msgstr "Liechtenstein"
-#: utils/countries.py:131
+#: src/utils/countries.py:132
msgid "Lithuania"
msgstr "Litauen"
-#: utils/countries.py:132
+#: src/utils/countries.py:133
msgid "Luxembourg"
msgstr "Luxemburg"
-#: utils/countries.py:133
+#: src/utils/countries.py:134
msgid "Macao"
msgstr "Macao"
-#: utils/countries.py:134
+#: src/utils/countries.py:135
msgid "Macedonia, The Former Yugoslav Republic of"
msgstr "Mazedonien, die ehemalige jugoslawische Republik"
-#: utils/countries.py:135
+#: src/utils/countries.py:136
msgid "Madagascar"
msgstr "Madagaskar"
-#: utils/countries.py:136
+#: src/utils/countries.py:137
msgid "Malawi"
msgstr "Malawi"
-#: utils/countries.py:137
+#: src/utils/countries.py:138
msgid "Malaysia"
msgstr "Malaysia"
-#: utils/countries.py:138
+#: src/utils/countries.py:139
msgid "Maldives"
msgstr "Malediven"
-#: utils/countries.py:139
+#: src/utils/countries.py:140
msgid "Mali"
msgstr "Mali"
-#: utils/countries.py:140
+#: src/utils/countries.py:141
msgid "Malta"
msgstr "Malta"
-#: utils/countries.py:141
+#: src/utils/countries.py:142
msgid "Marshall Islands"
msgstr "Marshall Islands"
-#: utils/countries.py:142
+#: src/utils/countries.py:143
msgid "Martinique"
msgstr "Martinique"
-#: utils/countries.py:143
+#: src/utils/countries.py:144
msgid "Mauritania"
msgstr "Mauretanien"
-#: utils/countries.py:144
+#: src/utils/countries.py:145
msgid "Mauritius"
msgstr "Mauritius"
-#: utils/countries.py:145
+#: src/utils/countries.py:146
msgid "Mayotte"
msgstr "Mayotte"
-#: utils/countries.py:146
+#: src/utils/countries.py:147
msgid "Mexico"
msgstr "Mexiko"
-#: utils/countries.py:147
+#: src/utils/countries.py:148
msgid "Micronesia, Federated States of"
msgstr "Mikronesien, Föderierte Staaten von"
-#: utils/countries.py:148
+#: src/utils/countries.py:149
msgid "Moldova"
msgstr "Moldawien"
-#: utils/countries.py:149
+#: src/utils/countries.py:150
msgid "Monaco"
msgstr "Monaco"
-#: utils/countries.py:150
+#: src/utils/countries.py:151
msgid "Mongolia"
msgstr "Mongolei"
-#: utils/countries.py:151
+#: src/utils/countries.py:152
msgid "Montenegro"
msgstr "Montenegro"
-#: utils/countries.py:152
+#: src/utils/countries.py:153
msgid "Montserrat"
msgstr "Montserrat"
-#: utils/countries.py:153
+#: src/utils/countries.py:154
msgid "Morocco"
msgstr "Marokko"
-#: utils/countries.py:154
+#: src/utils/countries.py:155
msgid "Mozambique"
msgstr "Mosambik"
-#: utils/countries.py:155
+#: src/utils/countries.py:156
msgid "Myanmar"
msgstr "Myanmar"
-#: utils/countries.py:156
+#: src/utils/countries.py:157
msgid "Namibia"
msgstr "Namibia"
-#: utils/countries.py:157
+#: src/utils/countries.py:158
msgid "Nauru"
msgstr "Nauru"
-#: utils/countries.py:158
+#: src/utils/countries.py:159
msgid "Nepal"
msgstr "Nepal"
-#: utils/countries.py:159
+#: src/utils/countries.py:160
msgid "Netherlands"
msgstr "Niederlande"
-#: utils/countries.py:160
+#: src/utils/countries.py:161
msgid "Netherlands Antilles"
msgstr "Niederländische Antillen"
-#: utils/countries.py:161
+#: src/utils/countries.py:162
msgid "New Caledonia"
msgstr "Neukaledonien"
-#: utils/countries.py:162
+#: src/utils/countries.py:163
msgid "New Zealand"
msgstr "New Zealand"
-#: utils/countries.py:163
+#: src/utils/countries.py:164
msgid "Nicaragua"
msgstr "Nicaragua"
-#: utils/countries.py:164
+#: src/utils/countries.py:165
msgid "Niger"
msgstr "Niger"
-#: utils/countries.py:165
+#: src/utils/countries.py:166
msgid "Nigeria"
msgstr "Nigeria"
-#: utils/countries.py:166
+#: src/utils/countries.py:167
msgid "Niue"
msgstr "Niue"
-#: utils/countries.py:167
+#: src/utils/countries.py:168
msgid "Norfolk Island"
msgstr "Norfolk Island"
-#: utils/countries.py:168
+#: src/utils/countries.py:169
msgid "Northern Mariana Islands"
msgstr "Northern Mariana Islands"
-#: utils/countries.py:169
+#: src/utils/countries.py:170
msgid "Norway"
msgstr "Norwegen"
-#: utils/countries.py:170
+#: src/utils/countries.py:171
msgid "Oman"
msgstr "Oman"
-#: utils/countries.py:171
+#: src/utils/countries.py:172
msgid "Pakistan"
msgstr "Pakistan"
-#: utils/countries.py:172
+#: src/utils/countries.py:173
msgid "Palau"
msgstr "Palau"
-#: utils/countries.py:173
+#: src/utils/countries.py:174
msgid "Palestinian Territory, Occupied"
msgstr "Palästinensische Autonomiegebiete"
-#: utils/countries.py:174
+#: src/utils/countries.py:175
msgid "Panama"
msgstr "Panama"
-#: utils/countries.py:175
+#: src/utils/countries.py:176
msgid "Papua New Guinea"
msgstr "Papua-Neuguinea"
-#: utils/countries.py:176
+#: src/utils/countries.py:177
msgid "Paraguay"
msgstr "Paraguay"
-#: utils/countries.py:177
+#: src/utils/countries.py:178
msgid "Peru"
msgstr "Peru"
-#: utils/countries.py:178
+#: src/utils/countries.py:179
msgid "Philippines"
msgstr "Philippinen"
-#: utils/countries.py:179
+#: src/utils/countries.py:180
msgid "Pitcairn"
msgstr "Pitcairn"
-#: utils/countries.py:180
+#: src/utils/countries.py:181
msgid "Poland"
msgstr "Polen"
-#: utils/countries.py:181
+#: src/utils/countries.py:182
msgid "Portugal"
msgstr "Portugal"
-#: utils/countries.py:182
+#: src/utils/countries.py:183
msgid "Puerto Rico"
msgstr "Puerto Rico"
-#: utils/countries.py:183
+#: src/utils/countries.py:184
msgid "Qatar"
msgstr "Katar"
-#: utils/countries.py:184
+#: src/utils/countries.py:185
msgid "Reunion"
msgstr "Wiedervereinigung"
-#: utils/countries.py:185
+#: src/utils/countries.py:186
msgid "Romania"
msgstr "Rumänien"
-#: utils/countries.py:186
+#: src/utils/countries.py:187
msgid "Russian Federation"
msgstr "Russischen Föderation"
-#: utils/countries.py:187
+#: src/utils/countries.py:188
msgid "Rwanda"
msgstr "Ruanda"
-#: utils/countries.py:188
+#: src/utils/countries.py:189
msgid "Saint Barthelemy"
msgstr "Saint Barthelemy"
-#: utils/countries.py:189
+#: src/utils/countries.py:190
msgid "Saint Helena"
msgstr "Saint Helena"
-#: utils/countries.py:190
+#: src/utils/countries.py:191
msgid "Saint Kitts and Nevis"
msgstr "Saint Kitts und Nevis"
-#: utils/countries.py:191
+#: src/utils/countries.py:192
msgid "Saint Lucia"
msgstr "Santa Lucia"
-#: utils/countries.py:192
+#: src/utils/countries.py:193
msgid "Saint Martin"
msgstr "Santa Martin"
-#: utils/countries.py:193
+#: src/utils/countries.py:194
msgid "Saint Pierre and Miquelon"
msgstr "Saint Pierre und Miquelon"
-#: utils/countries.py:194
+#: src/utils/countries.py:195
msgid "Saint Vincent and the Grenadines"
msgstr "Saint Vincent und die Grenadinen"
-#: utils/countries.py:195
+#: src/utils/countries.py:196
msgid "Samoa"
msgstr "Samoa"
-#: utils/countries.py:196
+#: src/utils/countries.py:197
msgid "San Marino"
msgstr "San Marino"
-#: utils/countries.py:197
+#: src/utils/countries.py:198
msgid "Sao Tome and Principe"
msgstr "Sao Tome und Principe"
-#: utils/countries.py:198
+#: src/utils/countries.py:199
msgid "Saudi Arabia"
msgstr "Saudi-Arabien"
-#: utils/countries.py:199
+#: src/utils/countries.py:200
msgid "Senegal"
msgstr "Senegal"
-#: utils/countries.py:200
+#: src/utils/countries.py:201
msgid "Serbia"
msgstr "Serbien"
-#: utils/countries.py:201
+#: src/utils/countries.py:202
msgid "Seychelles"
msgstr "Seychellen"
-#: utils/countries.py:202
+#: src/utils/countries.py:203
msgid "Sierra Leone"
msgstr "Sierra Leone"
-#: utils/countries.py:203
+#: src/utils/countries.py:204
msgid "Singapore"
msgstr "Singapur"
-#: utils/countries.py:204
+#: src/utils/countries.py:205
msgid "Slovakia"
msgstr "Slowakei"
-#: utils/countries.py:205
+#: src/utils/countries.py:206
msgid "Slovenia"
msgstr "Slowenien"
-#: utils/countries.py:206
+#: src/utils/countries.py:207
msgid "Solomon Islands"
msgstr "Salomon-Inseln"
-#: utils/countries.py:207
+#: src/utils/countries.py:208
msgid "Somalia"
msgstr "Somalia"
-#: utils/countries.py:208
+#: src/utils/countries.py:209
msgid "South Africa"
msgstr "Südafrika"
-#: utils/countries.py:209
+#: src/utils/countries.py:210
msgid "South Georgia and the South Sandwich Islands"
msgstr "Südgeorgien und die Südlichen Sandwichinseln"
-#: utils/countries.py:210
+#: src/utils/countries.py:211
msgid "Spain"
msgstr "Spanien"
-#: utils/countries.py:211
+#: src/utils/countries.py:212
msgid "Sri Lanka"
msgstr "Sri Lanka"
-#: utils/countries.py:212
+#: src/utils/countries.py:213
msgid "Sudan"
msgstr "Sudan"
-#: utils/countries.py:213
+#: src/utils/countries.py:214
msgid "Suriname"
msgstr "Suriname"
-#: utils/countries.py:214
+#: src/utils/countries.py:215
msgid "Svalbard and Jan Mayen"
msgstr "Svalbard und Jan Mayen"
-#: utils/countries.py:215
+#: src/utils/countries.py:216
msgid "Swaziland"
msgstr "Swaziland"
-#: utils/countries.py:216
+#: src/utils/countries.py:217
msgid "Sweden"
msgstr "Schweden"
-#: utils/countries.py:217
+#: src/utils/countries.py:218
msgid "Switzerland"
msgstr "Schweiz"
-#: utils/countries.py:218
+#: src/utils/countries.py:219
msgid "Syrian Arab Republic"
msgstr "Arabische Republik Syrien"
-#: utils/countries.py:219
+#: src/utils/countries.py:220
msgid "Taiwan, Province of China"
msgstr "Taiwan, Province of China"
-#: utils/countries.py:220
+#: src/utils/countries.py:221
msgid "Tajikistan"
msgstr "Tadschikistan"
-#: utils/countries.py:221
+#: src/utils/countries.py:222
msgid "Tanzania, United Republic of"
msgstr "Tansania, Vereinigte Republik"
-#: utils/countries.py:222
+#: src/utils/countries.py:223
msgid "Thailand"
msgstr "Thailand"
-#: utils/countries.py:223
+#: src/utils/countries.py:224
msgid "Timor-Leste"
msgstr "Timor-Leste"
-#: utils/countries.py:224
+#: src/utils/countries.py:225
msgid "Togo"
msgstr "Togo"
-#: utils/countries.py:225
+#: src/utils/countries.py:226
msgid "Tokelau"
msgstr "Tokelau"
-#: utils/countries.py:226
+#: src/utils/countries.py:227
msgid "Tonga"
msgstr "Tonga"
-#: utils/countries.py:227
+#: src/utils/countries.py:228
msgid "Trinidad and Tobago"
msgstr "Trinidad und Tobago"
-#: utils/countries.py:228
+#: src/utils/countries.py:229
msgid "Tunisia"
msgstr "Tunesien"
-#: utils/countries.py:229
+#: src/utils/countries.py:230
msgid "Turkey"
msgstr "Türkei"
-#: utils/countries.py:230
+#: src/utils/countries.py:231
msgid "Turkmenistan"
msgstr "Turkmenistan"
-#: utils/countries.py:231
+#: src/utils/countries.py:232
msgid "Turks and Caicos Islands"
msgstr "Turks-und Caicosinseln"
-#: utils/countries.py:232
+#: src/utils/countries.py:233
msgid "Tuvalu"
msgstr "Tuvalu"
-#: utils/countries.py:233
+#: src/utils/countries.py:234
msgid "Uganda"
msgstr "Uganda"
-#: utils/countries.py:234
+#: src/utils/countries.py:235
msgid "Ukraine"
msgstr "Ukraine"
-#: utils/countries.py:235
+#: src/utils/countries.py:236
msgid "United Arab Emirates"
msgstr "Vereinigte Arabische Emirate"
-#: utils/countries.py:236
+#: src/utils/countries.py:237
msgid "United States"
msgstr "Vereinigte Staaten"
-#: utils/countries.py:237
+#: src/utils/countries.py:238
msgid "United States Minor Outlying Islands"
msgstr "United States Minor Outlying Islands"
-#: utils/countries.py:238
+#: src/utils/countries.py:239
msgid "Uruguay"
msgstr "Uruguay"
-#: utils/countries.py:239
+#: src/utils/countries.py:240
msgid "Uzbekistan"
msgstr "Usbekistan"
-#: utils/countries.py:240
+#: src/utils/countries.py:241
msgid "Vanuatu"
msgstr "Vanuatu"
-#: utils/countries.py:241
+#: src/utils/countries.py:242
msgid "Venezuela"
msgstr "Venezuela"
-#: utils/countries.py:242
+#: src/utils/countries.py:243
msgid "Viet Nam"
msgstr "Vietnam"
-#: utils/countries.py:243
+#: src/utils/countries.py:244
msgid "Virgin Islands, British"
msgstr "Virgin Islands, British"
-#: utils/countries.py:244
+#: src/utils/countries.py:245
msgid "Virgin Islands, U.S."
msgstr "Virgin Islands, US"
-#: utils/countries.py:245
+#: src/utils/countries.py:246
msgid "Wallis and Futuna"
msgstr "Wallis und Futuna"
-#: utils/countries.py:246
+#: src/utils/countries.py:247
msgid "Western Sahara"
msgstr "Westsahara"
-#: utils/countries.py:247
+#: src/utils/countries.py:248
msgid "Yemen"
msgstr "Jemen"
-#: utils/countries.py:248
+#: src/utils/countries.py:249
msgid "Zambia"
msgstr "Sambia"
-#: utils/countries.py:249
+#: src/utils/countries.py:250
msgid "Zimbabwe"
msgstr "Zimbabwe"