From 0a45cf1fd85602e2b5b9ac8467860adf5d40f8ae 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 fd244f10e841d1015982ee402311695192af6af8 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 9276e97c36e08376e919886d87430f2bb8aefa91 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 c428f6ed1f653478beca17c3dd474d41dd1d04d1 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 36272c60d62d1addd7a8043f84f82cafffed7499 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 5ad628f33a60f592314e9bc65e2ba25b39905a12 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 bf12060c3bc981a4e951fbef75044d5f26977140 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 6de1ecb1026105ad9c58b8a348a30070541063d1 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 854fd387408f84384c346c9bf8f911e0954718d8 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 bb5081a78b947e1764ed585beca67c009a6bd66b 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 c5781246fe9e024ce41e1f2805b66bdfee22052c 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 ade2a568f7459ea9edca744ef0eb7aabb5edf6ac 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 c030a31e2bf76623b8a76d182325e66353010497 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 b7fab97715d298dcf84c02fa0920717f47d2db6f 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 9f6fffa4f4fb20297bc6e88a8946a3a9d4d7c688 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 fdbf8190925cd98926104c66674a193f8441570f 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 0b2e040fc9454b93aaa9e7581deab4997d711a44 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 4b0a5c9c82f2b3dcc98dd65d58d15af2b68e5f97 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' %}

    + +
    + {% csrf_token %} + {{ formset.management_form }} + + {% for form in formset %} +
    + {% for hidden in form.hidden_fields %} {{ hidden }} {% endfor %} +

    + + {{ form.start }} + {{ form.start.errors }} +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {{form.player1.label}}{{form.player2.label}}{{form.player3.label}}{{form.player4.label}}
    {{ form.player1 }}{{ form.player2 }}{{ form.player3 }}{{ form.player4 }}
    {{ form.player1_input_score }}{{ form.player2_input_score }}{{ form.player3_input_score }}{{ form.player4_input_score }}
    {{ form.player1.errors }} {{ form.player1_input_score.errors }}{{ form.player2.errors }} {{ form.player2_input_score.errors }}{{ form.player3.errors }} {{ form.player3_input_score.errors }}{{ form.player4.errors }} {{ form.player4_input_score.errors }}
    +

    + {{ form.comment }} + {{ form.comment.errors }} +

    +

    + + {{ form.DELETE }} {{form.DELETE.help_text}} + {{ form.DELETE.errors }} +

    +

    {{ form.non_field_errors }}

    +
    + {% endfor %} + {% if formset.errors %}
      {{ formset.errors|unordered_list }}
    {% endif %} + +

    + {% trans 'back' %} + +

    +
    +{% 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 0a793b79543ff1851611d0aa989934847661a2e3 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.mo | Bin 3144 -> 3305 bytes src/content/locale/de/LC_MESSAGES/django.po | 177 +++---- src/events/locale/de/LC_MESSAGES/django.mo | Bin 3952 -> 3707 bytes 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.mo | Bin 4832 -> 4415 bytes .../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.mo | Bin 10390 -> 9885 bytes .../locale/de/LC_MESSAGES/django.po | 359 ++++++------- src/utils/locale/de/LC_MESSAGES/django.po | 500 +++++++++--------- 21 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.mo b/src/content/locale/de/LC_MESSAGES/django.mo index 61bad0e44b8c877919322d1bbe86956c605c06c8..e1a691501fcd1b11c3a89dfb3fad0d824c07f96b 100644 GIT binary patch delta 1675 zcmZ|O%WGU!9Ki9DG|x$@X)7@nwWmrOX*-#jkkK|awvA08G>Ep5NTt*>GiPQlbLZai zKGG&G1{LZbaJ8%8viQJ7TXECuB2s)nAtJc2&_EZ0n}VA{LBGH0Ev^iA=5x>M_xqi5 zCb!1^e5QS8XU}JfzgPJ@%jfSdX({#dw*MUB^%vR$_&YvpcHx& zd+~dect7H9yqk_c#2)giUS=I&AcHSp5gAkOAdfnqw#z8<>L>{kltR|h@vA6>e4UP8 z$3@yV@i2}sIDzx{9A3gh5z)=pzQ2E%EE32rGO!n!giqqdJ$!( zIh4Dd!aaBzc~pZh8DGXMet`S&OO(QIqZImEhVz$+f6y_7TPTU2CJV1hql_O!S&&C5 z=oLJL$FPW(aUZUu#w{%1Hnu7Ai^xBSnn8(sB6Z;{UYM(va2J--_C*|{{V~d~dJQ=g z^%Xvk-=^(N>}&v~z^&ora?kpWncgE^ZO=5V_NzLoTir4>t4*k#NMCeZSDRMLnm{|A ze*4TqsO>~e*Q2O4oy%4Hrg6N9vfs>=T)&okx4DuH-MDr>rxqUvik!&pgj@0B@rrhx zhLy;XuS-^k)_S_MqBEVmamjVc$@_h8B=`D`wS6zL9urquR+R&5B3seEr>tj61SDsZ z;ZRkbAdK|m2_{l5GwF$2mCh1+0vp;$g=X2Rke!t4x?;m};Itx#fMMMP?41`XS7BtL zI8<*2$$R|^BZss3Lbg!QlatfM{K0%7S??dKJnEd(h2r#Nk z>(cn^^}q=u$9Q_y1~q-OVZ!+MCF@mIJSIDK!bGo)v(T%>oOgE7n&z}VY2zrQzN5+U zfh);B1EXC>lFfmax(idu=-}LOTVrF=jhjtFYgp2#l{9Sw#jey#g)w=_j_z*D z%(N5)7s*8ty^H}5B6#s8T7m~LQVKnYAP9me*c@6a{sZw?Pkw(pgMu&f%;(*A-g*B$ zyZh_NSH;fGaNnk(%n=U~H!{X7VY8PT<&8dL2JubI;U?aPA7DRzocbB=;`5iNe&1jg zzsEe@Oxu4!z4sR~PRHC$8*=v~9fxo)9Y#?TSFwN#IDn^613ZtK;5^=oYp96>SkV3Ex9K-$JG|+c?Sm<~o)AH2jf1*u|(+%^p<85>Db6YJiidg`7>FpGPk95;r~f zaT)`B2tUMeyoQJH4r<(CcG1K9W*-&3SVA4u0n|}WqZYD&df_;#-*Wo-Ib1OQGi;&734A--1Oqxr~%(aE#MId;V zYQR5{%lyr4H)h$szJw)Y(`F*Iiki5Fn)oRsQ~#Dnm4D?-FKI>k;#7!72$g-r6Wv;} z^F7qHGfRvTRYED4CG_*vBD5ft2NJq>ZAztwIM}TvC)H7&bOI_PgpO6YR#7S*?$(m@ zD0j;51W`=ebt;pDequV2DMG2z=^Y^SzoHY+sp$kbfNps_K|?0xP?z}Y-tDco`VME} zmF)6(z2S#8@}s7+-o=Z~3qtF+ZPak~M1AG7Z8&eu1@UHfF}{^8nzUT7`wR(t)Dx9-Yy=e6eTDKETaYnNT%{PyB2e4zV(o#ySD z%daf2JR2X&ZNxw2#xvFUT5cpW8}|-;yR|dmW#a9@rTxcUVC#P5n$`v3YJ;I7>$j4< Wo=EoEQ#cu)E7\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.mo b/src/events/locale/de/LC_MESSAGES/django.mo index 58269194c092480d88ee27b8c02c318a488d86ee..01454b99e7de6869c8fbecc8370aad0eec4ac7f5 100644 GIT binary patch delta 1360 zcmYMzOGs2v9LMpKqnTrwnl!zPYeq&gF*9hyP-YJzD9U7l7WTO2ZJHeK$jlw-0SyGz zLPEl#S|mXbL=i*~1U;Zu5mex!O@tIdi=q}is380P>OgYme$M%y*Z-Vz{k-tkg3{~y z@^gm24g41HTUDl-F+Jt~ZK1e}dKI3qj^I@47jYV1#!9?~JmwZJwco*NypPN935M|t zhKwnhNeUGNLcC^R6>2~Pr(?5?$85a=8;Ea4EwC4rK-xNpJmxSjwU>}3%t=(gVe46} zVSRInLOl&P{sxSBh#K$=XW}?2k+-M`Kcf=-ZsQ?>wbW~I4mP32C$SFK*!Jz#UYtd| zfEBE7PEpX&p2iwHk6ZC7YND^GM1CTV33JY@ZR$|<2#(=GRR8;^ojyX2*gQog_}sQn zpb~qBrP&m|Q_z4KUYa<9ny3*KUu5I0sLvyg3^8qZZwlWGCaM2m7us1H7pxQ8gxc5+ z)Sd6)rFjpc*2#rAf5w_30S!2YQ5;5Pco#L`DQc%LP|VCq~x>GZa&KakH9 zf{g1s>%4s0S=H|rik*i%zwfY5=Zv@94LVvF=%|JBy_#Vwl6N${kl*(xFRfA5{Ow3GD8;2ectqe?P z5s^?qxXYq8t|I8Mh;So>K~MyOV2cW@MZdpYp$_x^e)rz*+88FdO#S@r3PPfy)^mg8a;V9tvR0@(I*>6Of;I!9xMPf%1E2 zIR!oJZ$8n;=f&K@08x>KCgj6hSO^u!W{8Waf(o#~j<>-)`rWVyhM?vT!e#KPeLrIP z5Ed~045qTbc}qj3eGfhG1MGk^Pz%))wgPE_3ZMfj!yel|4)4-G3FYr%F`6a~Qeu+} zHC_N$!Xo>ACroOgI_uD6U$j7tw?nRj>4L;!df-!+F(+V)wP~cf|JZWK(~YXp{l*0n`yZf-3oQ%U4hfOxgZ4)CRLqnf-wL%x@kF zG=rPG3YI|KiCU=2bV60E7s@Y^w2jM98Qp@4a15%1kD)S}fL-tg@mCAmX9nrbyGRc}yE!v3mw_I<=d))&mDY59e;UDS4$r_)( z-d9oW4O9mGf$jc)-=Eoe$k!Db2*<\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 "" " \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
\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"