Anfänglicher Commit: Producion Version Stand: Oktober 2014
This commit is contained in:
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
1
src/aggregator/__init__.py
Normal file
1
src/aggregator/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
17
src/aggregator/admin.py
Normal file
17
src/aggregator/admin.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from django.contrib import admin
|
||||
from models import Feed, FeedItem
|
||||
|
||||
admin.site.register(Feed,
|
||||
list_display=["title", "public_url", "last_update", 'is_functional'],
|
||||
list_filter=["is_functional"],
|
||||
ordering=["title"],
|
||||
search_fields=["title", "public_url"],
|
||||
list_per_page=500,
|
||||
)
|
||||
|
||||
admin.site.register(FeedItem,
|
||||
list_display=['title', 'feed', 'date_modified'],
|
||||
list_filter=['feed'],
|
||||
search_fields=['feed__title', 'feed__public_url', 'title'],
|
||||
date_heirarchy=['date_modified'],
|
||||
)
|
||||
23
src/aggregator/feeds.py
Normal file
23
src/aggregator/feeds.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import django.contrib.syndication.views
|
||||
from .models import FeedItem
|
||||
|
||||
|
||||
class LatestFeedItems(django.contrib.syndication.views.Feed):
|
||||
link = "http://aol.animanga.at/"
|
||||
description = "Aktuelle Nachrichten aus der Austrian Otaku League"
|
||||
title = "AOL - Newsfeed"
|
||||
|
||||
def items(self, obj):
|
||||
return FeedItem.objects.get_recent_items()
|
||||
|
||||
def item_title(self, item):
|
||||
return "%s: %s" % (item.feed.title, item.title)
|
||||
|
||||
def item_description(self, item):
|
||||
return item.summary
|
||||
|
||||
def item_author_name(self, item):
|
||||
return item.feed.title
|
||||
|
||||
def item_pubdate(self, item):
|
||||
return item.date_modified
|
||||
1
src/aggregator/management/__init__.py
Normal file
1
src/aggregator/management/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# -
|
||||
1
src/aggregator/management/commands/__init__.py
Normal file
1
src/aggregator/management/commands/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
#
|
||||
59
src/aggregator/management/commands/update-feeds.py
Normal file
59
src/aggregator/management/commands/update-feeds.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
Update feeds for Django community page. Requires Mark Pilgrim's excellent
|
||||
Universal Feed Parser (http://feedparser.org)
|
||||
"""
|
||||
|
||||
from aggregator.models import Feed
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Updates all RSS Feeds"
|
||||
|
||||
def parse_feed(self, feed, verbose=True):
|
||||
parsed_feed = feedparser.parse(feed.feed_url)
|
||||
html_parser = HTMLParser.HTMLParser()
|
||||
for entry in parsed_feed:
|
||||
title = entry.title.encode(parsed_feed.encoding, "xmlcharrefreplace")
|
||||
guid = entry.get("id", entry.link).encode(parsed_feed.encoding, "xmlcharrefreplace")
|
||||
link = entry.link.encode(parsed_feed.encoding, "xmlcharrefreplace")
|
||||
if verbose:
|
||||
print '>' , title
|
||||
|
||||
if not guid:
|
||||
guid = link
|
||||
|
||||
if hasattr(entry, "summary"):
|
||||
content = entry.summary
|
||||
elif hasattr(entry, "content"):
|
||||
content = entry.content[0].value
|
||||
elif hasattr(entry, "description"):
|
||||
content = entry.description
|
||||
else:
|
||||
content = u""
|
||||
content = content.encode(parsed_feed.encoding, "xmlcharrefreplace")
|
||||
|
||||
try:
|
||||
if entry.has_key('modified_parsed'):
|
||||
date_modified = datetime.fromtimestamp(time.mktime(entry.modified_parsed))
|
||||
elif parsed_feed.feed.has_key('modified_parsed'):
|
||||
date_modified = datetime.fromtimestamp(time.mktime(parsed_feed.feed.modified_parsed))
|
||||
elif parsed_feed.has_key('modified'):
|
||||
date_modified = datetime.fromtimestamp(time.mktime(parsed_feed.modified))
|
||||
else:
|
||||
date_modified = datetime.now()
|
||||
except TypeError:
|
||||
date_modified = datetime.now()
|
||||
|
||||
try:
|
||||
feed.feeditem_set.get(guid=guid)
|
||||
except FeedItem.DoesNotExist:
|
||||
feed.feeditem_set.create(title=title, link=link, summary=content, guid=guid, date_modified=date_modified)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
verbose = int(options['verbosity']) > 0
|
||||
for feed in Feed.objects.filter(is_functional=True):
|
||||
if (verbose):
|
||||
print
|
||||
print "%s - URL: %s" % (feed.title, feed.feed_url)
|
||||
print '=' * 80
|
||||
feed.parse()
|
||||
101
src/aggregator/models.py
Normal file
101
src/aggregator/models.py
Normal file
@@ -0,0 +1,101 @@
|
||||
'''
|
||||
Created on 05.02.2011
|
||||
|
||||
@author: christian
|
||||
'''
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site
|
||||
from django.utils import timezone
|
||||
from utils.html5 import models
|
||||
import HTMLParser
|
||||
import django.db.models
|
||||
import feedparser
|
||||
import urllib2
|
||||
|
||||
class FeedManager(django.db.models.Manager):
|
||||
|
||||
def active(self):
|
||||
site = settings.SITE_ID
|
||||
feeds = self.filter(is_functional=True, site=site)
|
||||
for feed in feeds:
|
||||
if feed.last_update:
|
||||
feed_age = timezone.now() - feed.last_update
|
||||
if feed_age > timedelta(hours=12):
|
||||
feed.parse()
|
||||
else:
|
||||
feed.parse()
|
||||
return feeds
|
||||
|
||||
class FeedItemManager(django.db.models.Manager):
|
||||
|
||||
def recent_items(self, max_items=10, site=None):
|
||||
site = site or settings.SITE_ID
|
||||
return self.select_related().filter(feed__site=site)[:max_items]
|
||||
|
||||
class Feed(models.Model):
|
||||
title = models.CharField(max_length=500)
|
||||
site = models.ForeignKey(Site)
|
||||
feed_url = models.URLField(unique=True, max_length=255)
|
||||
public_url = models.URLField(max_length=255)
|
||||
last_update = models.DateTimeField(blank=True, null=True)
|
||||
is_functional = models.BooleanField(default=True)
|
||||
objects = FeedManager()
|
||||
|
||||
def __unicode__(self):
|
||||
return self.title
|
||||
|
||||
def parse(self):
|
||||
parsed_feed = feedparser.parse(self.feed_url)
|
||||
html_parser = HTMLParser.HTMLParser()
|
||||
|
||||
if parsed_feed.bozo and type(parsed_feed.bozo_exception) == urllib2.URLError:
|
||||
self.is_functional = False
|
||||
return self.save()
|
||||
|
||||
for feed_entry in parsed_feed.entries:
|
||||
title = html_parser.unescape(feed_entry.title)
|
||||
if not title:
|
||||
continue
|
||||
|
||||
link = feed_entry.link
|
||||
guid = feed_entry.get("id", link)
|
||||
summary = html_parser.unescape(feed_entry.get("summary",
|
||||
feed_entry.get("description", feed_entry.get("content", u""))
|
||||
))
|
||||
date_modified = feed_entry.get("published_parsed", parsed_feed.get("published_parsed", timezone.now))
|
||||
date_modified = timezone.make_aware(datetime(*date_modified[:6]), timezone.get_current_timezone())
|
||||
|
||||
feed_item, updated = self.feed_items.get_or_create(guid=guid,
|
||||
defaults={
|
||||
'title': title,
|
||||
'link': link,
|
||||
'summary': summary,
|
||||
'date_modified': date_modified
|
||||
})
|
||||
feed_item.save()
|
||||
self.last_update = timezone.now()
|
||||
return self.save()
|
||||
|
||||
class Meta:
|
||||
ordering = ("title",)
|
||||
|
||||
|
||||
class FeedItem(models.Model):
|
||||
feed = models.ForeignKey(Feed, related_name='feed_items')
|
||||
title = models.CharField(max_length=500)
|
||||
link = models.URLField(max_length=500)
|
||||
guid = models.CharField(max_length=255, unique=True, db_index=True)
|
||||
summary = models.TextField(blank=True)
|
||||
date_modified = models.DateTimeField()
|
||||
objects = FeedItemManager()
|
||||
|
||||
class Meta:
|
||||
ordering = ("-date_modified",)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.title
|
||||
|
||||
def get_absolute_url(self):
|
||||
return self.link
|
||||
12
src/aggregator/sitemaps.py
Normal file
12
src/aggregator/sitemaps.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from django.contrib.sitemaps import Sitemap
|
||||
from models import FeedItem
|
||||
|
||||
class FeedItemSitemap(Sitemap):
|
||||
changefreq = "never"
|
||||
priority = 0.5
|
||||
|
||||
def items(self):
|
||||
return FeedItem.objects.get_recent_items()
|
||||
|
||||
def lastmod(self, obj):
|
||||
return obj.date_modified
|
||||
29
src/aggregator/templatetags/aggregator.py
Normal file
29
src/aggregator/templatetags/aggregator.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from django import template
|
||||
from aggregator.models import Feed
|
||||
|
||||
|
||||
class FeedListNode(template.Node):
|
||||
|
||||
def __init__(self, varname):
|
||||
self.varname = varname
|
||||
|
||||
def render(self, context):
|
||||
context[self.varname] = Feed.objects.filter(is_defunct=False)
|
||||
return ''
|
||||
|
||||
|
||||
def do_get_feed_list(parser, token):
|
||||
"""
|
||||
{% get_feed_list as feed_list %}
|
||||
"""
|
||||
bits = token.contents.split()
|
||||
if len(bits) != 3:
|
||||
raise template.TemplateSyntaxError, \
|
||||
"'%s' tag takes two arguments" % bits[0]
|
||||
if bits[1] != "as":
|
||||
raise template.TemplateSyntaxError, \
|
||||
"First argument to '%s' tag must be 'as'" % bits[0]
|
||||
return FeedListNode(bits[2])
|
||||
|
||||
register = template.Library()
|
||||
register.tag('get_feed_list', do_get_feed_list)
|
||||
4
src/backup.log
Normal file
4
src/backup.log
Normal file
@@ -0,0 +1,4 @@
|
||||
#1406838232 Disk /dev/sdi - 1031 MB / 983 MiB - CHS 46 228 48
|
||||
1 : start= 48, size= 2013648, Id=0B, *
|
||||
#1406838486 Disk /dev/sdi - 188 MB / 180 MiB - CHS 40 48 48
|
||||
1 : start= 48, size= 504528, Id=0B, *
|
||||
0
src/content/__init__.py
Normal file
0
src/content/__init__.py
Normal file
60
src/content/admin.py
Normal file
60
src/content/admin.py
Normal file
@@ -0,0 +1,60 @@
|
||||
'''
|
||||
Created on 19.09.2011
|
||||
|
||||
@author: christian
|
||||
'''
|
||||
# import stuff we need from django
|
||||
from django.contrib import admin
|
||||
from . import models
|
||||
|
||||
|
||||
class PageTabularInline(admin.TabularInline):
|
||||
fields = ('title_de', 'menu_name_de', "position",)
|
||||
model = models.Page
|
||||
sortable_field_name = "position"
|
||||
|
||||
|
||||
class ArticleAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {"slug": ("headline_de",)}
|
||||
list_display = ('headline', 'category', 'date_created', 'author',)
|
||||
list_editable = ('category', 'author')
|
||||
|
||||
|
||||
class CategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'description']
|
||||
list_display_links = ('name', 'slug',)
|
||||
prepopulated_fields = {'slug': ('name_de',)}
|
||||
|
||||
|
||||
class PageAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {"slug": ('menu_name_de',)}
|
||||
inlines = [PageTabularInline, ]
|
||||
list_display = ('position', 'menu_name', 'title', 'parent', 'path',)
|
||||
list_display_links = ('menu_name', 'title',)
|
||||
list_editable = ('position',)
|
||||
list_filter = ('parent',)
|
||||
search_fields = ('menu_name_de', 'title_de',)
|
||||
fieldsets = (
|
||||
('Deutsch', {
|
||||
'fields': ('menu_name_de', 'title_de', 'pdf_de', 'content_de',),
|
||||
'classes': ('collapse',),
|
||||
}),
|
||||
('English', {
|
||||
'fields': ('menu_name_en', 'title_en', 'pdf_en', 'content_en'),
|
||||
'classes': ('collapse', 'closed'),
|
||||
}),
|
||||
('Meta Data', {
|
||||
'fields': ('content_type', 'slug', ('parent', 'position'),
|
||||
'status', 'template',)
|
||||
}),
|
||||
)
|
||||
|
||||
class Media:
|
||||
js = [
|
||||
'/static/js/tiny_mce/tiny_mce.js',
|
||||
'/static/js/tinymce_setup.js',
|
||||
]
|
||||
|
||||
admin.site.register(models.Article, ArticleAdmin)
|
||||
admin.site.register(models.Page, PageAdmin)
|
||||
admin.site.register(models.Category, CategoryAdmin)
|
||||
49
src/content/context_processors.py
Normal file
49
src/content/context_processors.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# -*- encoding: UTF-8 -*-
|
||||
'''
|
||||
Created on 30.09.2011
|
||||
|
||||
@author: christian
|
||||
'''
|
||||
from . import models
|
||||
from django.core.cache import cache
|
||||
|
||||
|
||||
def content_menus(request):
|
||||
current_page = None
|
||||
current_path = request.path_info[1:request.path_info.rfind('.')]
|
||||
current_top_page = None
|
||||
|
||||
# erzeuge das Top-Level Menü
|
||||
top_menu_items = []
|
||||
top_level_pages = cache.get('top_level_pages')
|
||||
if top_level_pages == None:
|
||||
top_level_pages = models.Page.objects.filter(parent=None)
|
||||
top_level_pages = top_level_pages.exclude(slug='index')
|
||||
top_level_pages = top_level_pages.order_by('position')
|
||||
cache.set('top_level_pages', top_level_pages, 360)
|
||||
for item in top_level_pages:
|
||||
if current_path.startswith(item.path):
|
||||
item.active = True
|
||||
current_top_page = item
|
||||
else:
|
||||
item.active = False
|
||||
top_menu_items.append(item)
|
||||
|
||||
# Entdecke die aktuell geöffnete Seite
|
||||
all_pages = cache.get('all_pages')
|
||||
if all_pages == None:
|
||||
all_pages = models.Page.objects.values_list('path', 'id')
|
||||
all_pages = dict((path, page_id) for path, page_id in all_pages)
|
||||
cache.set('all_pages', all_pages, 360)
|
||||
|
||||
while len(current_path) > 0:
|
||||
if current_path in all_pages:
|
||||
current_page = models.Page.objects.get(pk=all_pages[current_path])
|
||||
break
|
||||
current_path = current_path[0:current_path.rfind('.')]
|
||||
|
||||
return {'top_menu_items': top_menu_items,
|
||||
'current_top_page': current_top_page,
|
||||
'current_path': current_path,
|
||||
'current_page': current_page
|
||||
}
|
||||
61
src/content/feeds.py
Normal file
61
src/content/feeds.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from datetime import datetime, time
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.contrib import comments
|
||||
from django.contrib.syndication.views import Feed
|
||||
from django.utils.feedgenerator import Rss201rev2Feed
|
||||
from models import Article
|
||||
|
||||
|
||||
class LatestNews(Feed):
|
||||
link = "http://www.kasu.at/"
|
||||
description = _("Current news from Kasu")
|
||||
title = "Kasu - traditonelle asiatische Spielkultur"
|
||||
feed_type = Rss201rev2Feed
|
||||
|
||||
def items(self, obj):
|
||||
return Article.objects.published()[:10]
|
||||
|
||||
def item_title(self, item):
|
||||
return item.headline
|
||||
|
||||
def item_author_name(self, item):
|
||||
return item.author.username
|
||||
|
||||
def item_categories(self, item):
|
||||
return (item.category.name,)
|
||||
|
||||
def item_description(self, item):
|
||||
return item.content
|
||||
|
||||
def item_pubdate(self, item):
|
||||
return datetime.combine(item.date_created, time())
|
||||
|
||||
|
||||
class LatestComments(Feed):
|
||||
"""Feed of latest comments on the current site."""
|
||||
|
||||
link = "http://www.kasu.at/"
|
||||
description = _("Latest comments on kasu.at")
|
||||
title = _("Kasu - latest comments")
|
||||
feed_type = Rss201rev2Feed
|
||||
|
||||
def items(self):
|
||||
qs = comments.get_model().objects.filter(site__pk=settings.SITE_ID,
|
||||
is_public=True, is_removed=False)
|
||||
return qs.order_by('-submit_date')[:40]
|
||||
|
||||
def item_author_name(self, item):
|
||||
return item.user_name
|
||||
|
||||
def item_description(self, item):
|
||||
return item.comment
|
||||
|
||||
def item_pubdate(self, item):
|
||||
return item.submit_date
|
||||
|
||||
def item_title(self, item):
|
||||
return 'From %(user_name)s in %(content_object)s' % {
|
||||
'user_name': item.user_name,
|
||||
'content_object': item.content_object
|
||||
}
|
||||
58
src/content/forms.py
Normal file
58
src/content/forms.py
Normal file
@@ -0,0 +1,58 @@
|
||||
'''
|
||||
Created on 04.10.2011
|
||||
|
||||
@author: christian
|
||||
'''
|
||||
import django.forms
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from utils.html5 import forms
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
class ArticleForm(forms.ModelForm):
|
||||
error_css_class = 'error'
|
||||
required_css_class = 'required'
|
||||
|
||||
class Meta(object):
|
||||
fields = ('headline_de', 'content_de', 'headline_en', 'content_en',
|
||||
'category', 'image')
|
||||
model = models.Article
|
||||
|
||||
def save(self, force_insert=False, force_update=False, commit=True):
|
||||
article = super(ArticleForm, self).save(commit=False)
|
||||
article.slug = slugify(article.headline_de)[:50]
|
||||
if commit:
|
||||
article.save(force_insert=force_insert, force_update=force_update)
|
||||
return article
|
||||
|
||||
|
||||
class PageForm(forms.ModelForm):
|
||||
error_css_class = 'error'
|
||||
required_css_class = 'required'
|
||||
content_type = django.forms.ChoiceField(
|
||||
choices=models.CONTENT_CHOICES,
|
||||
widget=django.forms.RadioSelect
|
||||
)
|
||||
|
||||
class Meta(object):
|
||||
exclude = ('position',)
|
||||
model = models.Page
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super(PageForm, self).clean()
|
||||
content_type = cleaned_data.get("content_type")
|
||||
pdf_de = cleaned_data.get("pdf_de")
|
||||
if not pdf_de and content_type == "2":
|
||||
msg = _('Please upload a PDF-File to this PDF-Page.')
|
||||
self._errors["content_type"] = self.error_class([msg])
|
||||
self._errors["pdf_de"] = self.error_class([msg])
|
||||
# These fields are no longer valid. Remove them from the
|
||||
# cleaned data.
|
||||
del cleaned_data["content_type"]
|
||||
del cleaned_data["pdf_de"]
|
||||
|
||||
# Always return the full collection of cleaned data.
|
||||
return cleaned_data
|
||||
1
src/content/management/__init__.py
Normal file
1
src/content/management/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
#
|
||||
1
src/content/management/commands/__init__.py
Normal file
1
src/content/management/commands/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
#!/usr/bin/python
|
||||
73
src/content/management/commands/importarticles.py
Normal file
73
src/content/management/commands/importarticles.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from content.models import Article, Category
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.utils.datetime_safe import datetime
|
||||
import re
|
||||
import xlrd #@UnresolvedImport
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Importiert die alten Daten aus einer CSV Datei" #@ReservedAssignment
|
||||
date_header_regex = r"""<h1><span class=\"small\">(?P<date>[\d\.]*)[\ -]*</span>[\ -]*(?P<title>.*)</h1>(?P<content>.*)"""
|
||||
header_regex = r"""<h1>[\ -]*(?P<title>.*)</h1>(?P<content>.*)"""
|
||||
|
||||
def __init__(self):
|
||||
self.author = User.objects.get(username="xeniac")
|
||||
self.category = Category.objects.get(slug='allgemeines')
|
||||
|
||||
def create_article(self):
|
||||
self.slug = slugify(self.headline[:50])
|
||||
article, created = Article.objects.get_or_create(slug=self.slug, date_created=self.date_created,
|
||||
defaults={
|
||||
'author': self.author,
|
||||
'headline_de': self.headline,
|
||||
'content_de': self.content,
|
||||
'category': self.category
|
||||
})
|
||||
if created:
|
||||
print "Created: %s - %s" % (self.date_created, self.headline)
|
||||
article.clean()
|
||||
article.save()
|
||||
|
||||
def parse_with_date(self, original):
|
||||
match_obj = re.search(self.date_header_regex, original, re.IGNORECASE | re.DOTALL)
|
||||
if match_obj:
|
||||
self.date_created = datetime.strptime(match_obj.group('date'), '%d.%m.%Y')
|
||||
self.headline = match_obj.group('title').strip()
|
||||
self.content = match_obj.group('content').strip()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def parse_without_date(self, original):
|
||||
match_obj = re.search(self.header_regex, original, re.IGNORECASE | re.DOTALL)
|
||||
if match_obj:
|
||||
self.date_created = datetime.strptime('01.01.1982', '%d.%m.%Y')
|
||||
self.headline = match_obj.group('title').strip()
|
||||
self.content = match_obj.group('content').strip()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def handle(self, *args, **options):
|
||||
try:
|
||||
xls_file = xlrd.open_workbook(args[0])
|
||||
except IndexError:
|
||||
print "Bitte den Pfad zur CSV Datei angeben!"
|
||||
return False
|
||||
except IOError:
|
||||
print "Datei '%s' wurde nicht gefunden! " % args[0]
|
||||
return False
|
||||
|
||||
table = xls_file.sheet_by_index(0)
|
||||
for row in xrange(1, table.nrows):
|
||||
if not table.cell_value(row, 2) in ('Archiv', 'News'):
|
||||
continue
|
||||
original = table.cell_value(row, 3)
|
||||
if self.parse_with_date(original) or self.parse_without_date(original):
|
||||
self.create_article()
|
||||
else:
|
||||
print "Fehler bei String!"
|
||||
print table.cell_value(row, 3)
|
||||
42
src/content/management/commands/importgalleries.py
Normal file
42
src/content/management/commands/importgalleries.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.datetime_safe import datetime
|
||||
from events.models import Event, Location
|
||||
import xlrd # @UnresolvedImport
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Importiert die alten Events" # @ReservedAssignment
|
||||
|
||||
def __init__(self):
|
||||
self.author = User.objects.get(username="xeniac")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
try:
|
||||
xls_file = xlrd.open_workbook(args[0])
|
||||
except IndexError:
|
||||
return "Bitte den Pfad zur Excel Datei angeben!"
|
||||
except IOError:
|
||||
return "Datei '%s' wurde nicht gefunden! " % args[0]
|
||||
|
||||
table = xls_file.sheet_by_index(0)
|
||||
for row in xrange(1, table.nrows):
|
||||
|
||||
name = table.cell_value(row, 0)
|
||||
print name
|
||||
|
||||
start = xlrd.xldate_as_tuple(table.cell_value(row, 1),
|
||||
xls_file.datemode)
|
||||
start = datetime(start[0], start[1], start[2], 0, 0, 0)
|
||||
|
||||
end = xlrd.xldate_as_tuple(table.cell_value(row, 2),
|
||||
xls_file.datemode)
|
||||
end = datetime(end[0], end[1], end[2], 23, 59, 59)
|
||||
|
||||
location = Location.objects.get(pk=table.cell_value(row, 3))
|
||||
|
||||
Event.objects.get_or_create(name=name, start=start, defaults={
|
||||
'end': end,
|
||||
'location': location,
|
||||
})
|
||||
262
src/content/models.py
Normal file
262
src/content/models.py
Normal file
@@ -0,0 +1,262 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from os import path
|
||||
from django.utils.timezone import now
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import get_language, ugettext as _
|
||||
from imagekit.models import ImageSpecField
|
||||
from imagekit.processors import SmartResize
|
||||
from utils import STATUS_CHOICES, STATUS_WAITING, STATUS_PUBLISHED, \
|
||||
STATUS_REJECTED, cleaner, OverwriteStorage # @UnusedImport
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
CONTENT_CHOICES = (
|
||||
(0, u'Django View'),
|
||||
(1, u'HTML'),
|
||||
(2, u'PDF')
|
||||
)
|
||||
|
||||
|
||||
def get_upload_path(instance, filename):
|
||||
'''
|
||||
Generates the desired file path and filename for an uploaded Image.
|
||||
With this function Django can save the uploaded images to subfolders that
|
||||
also have a meaning for humans.
|
||||
|
||||
@param instance: an Django Object for which the Image has been uploaded.
|
||||
@type instance: a instace of an models.Model sub-class.
|
||||
@param filename: The filename of the uploaded image.
|
||||
@type filename: String
|
||||
'''
|
||||
extension = filename[filename.rfind('.') + 1:]
|
||||
if isinstance(instance, Category):
|
||||
return "categories/%s.%s" % (instance.slug, extension)
|
||||
|
||||
|
||||
class ImageModel(models.Model):
|
||||
article = ImageSpecField(
|
||||
source='image',
|
||||
processors=[SmartResize(width=210, height=130)]
|
||||
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class ArticleManager(models.Manager):
|
||||
def published(self):
|
||||
return self.filter(status=STATUS_PUBLISHED, date_created__lte=now())
|
||||
|
||||
|
||||
class Article(ImageModel):
|
||||
headline_de = models.CharField(_('Headline'), max_length=255)
|
||||
headline_en = models.CharField('Headline', max_length=255, blank=True)
|
||||
content_de = models.TextField(_('Content'))
|
||||
content_en = models.TextField('Content', blank=True)
|
||||
category = models.ForeignKey('Category', 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('auth.User', verbose_name=_('Author'))
|
||||
status = models.SmallIntegerField(_('Status'), choices=STATUS_CHOICES,
|
||||
default=STATUS_PUBLISHED)
|
||||
date_created = models.DateField(_('Created'), blank=True)
|
||||
date_modified = models.DateTimeField(_('Modified'), auto_now=True)
|
||||
objects = ArticleManager()
|
||||
|
||||
class Meta(object):
|
||||
verbose_name = _('Article')
|
||||
verbose_name_plural = _('Articles')
|
||||
ordering = ('-date_created',)
|
||||
|
||||
def clean(self):
|
||||
if not self.date_created:
|
||||
self.date_created = now()
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.headline_de)[:50]
|
||||
self.content_de = cleaner.clean_html(self.content_de)
|
||||
self.content_en = cleaner.clean_html(self.content_en)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.headline
|
||||
|
||||
@property
|
||||
def posting_image(self):
|
||||
if self.image:
|
||||
return self.article
|
||||
else:
|
||||
return self.category.article
|
||||
|
||||
def get_absolute_url(self):
|
||||
kwargs = {
|
||||
'year': self.date_created.strftime('%Y'),
|
||||
'month': self.date_created.strftime('%m'),
|
||||
'slug': self.slug,
|
||||
}
|
||||
return reverse('show-article', kwargs=kwargs)
|
||||
|
||||
@property
|
||||
def headline(self):
|
||||
headline = getattr(self, "headline_%s" % get_language())
|
||||
if not headline:
|
||||
return mark_safe(self.headline_de)
|
||||
else:
|
||||
return mark_safe(headline)
|
||||
|
||||
@property
|
||||
def content(self):
|
||||
content = getattr(self, "content_%s" % get_language(), self.content_de)
|
||||
if not content:
|
||||
return mark_safe(self.content_de)
|
||||
else:
|
||||
return mark_safe(content)
|
||||
|
||||
|
||||
class Page(models.Model):
|
||||
'''
|
||||
Eine Seite auf der Homepage. Sie kann eine "statische" HTML Seite,
|
||||
die URL einer dynamische Django View, oder ein PDF Dokument sein.
|
||||
Jede Seite kann neben Deutsch auch auf Englisch angeboten werden.
|
||||
Ist keine englische Übersetzung vorhanden, wird die deutsche Version
|
||||
angeboten.
|
||||
'''
|
||||
menu_name_de = models.CharField(
|
||||
'Menü Name',
|
||||
max_length=255,
|
||||
help_text=_('The short name for the menu-entry of this page')
|
||||
)
|
||||
menu_name_en = models.CharField(
|
||||
'Menu Name',
|
||||
max_length=255,
|
||||
blank=True,
|
||||
help_text=_('The short name for the menu-entry of this page')
|
||||
)
|
||||
title_de = models.CharField('Titel', max_length=255,
|
||||
help_text=_('This title appears in the HTML header'))
|
||||
title_en = models.CharField('Title', max_length=255, blank=True,
|
||||
help_text=_('This title appears in the HTML header'))
|
||||
slug = models.SlugField(_('slug'))
|
||||
path = models.CharField(_('Path'), max_length=100, db_index=True,
|
||||
editable=False, unique=True)
|
||||
|
||||
parent = models.ForeignKey('self', blank=True, null=True,
|
||||
related_name='subpages', on_delete=models.SET_NULL)
|
||||
position = models.PositiveSmallIntegerField(_('Position'),
|
||||
blank=True, null=True)
|
||||
status = models.SmallIntegerField(_('status'), choices=STATUS_CHOICES,
|
||||
default=STATUS_WAITING)
|
||||
content_type = models.IntegerField(choices=CONTENT_CHOICES)
|
||||
|
||||
content_de = models.TextField('Inhalt', blank=True)
|
||||
content_en = models.TextField('Content', blank=True)
|
||||
enable_comments = models.BooleanField(_('enable comments'), default=True)
|
||||
template = models.CharField(_('Template'), max_length=100,
|
||||
default="content/page.html")
|
||||
pdf_de = models.FileField(upload_to='pdf/de/', blank=True, null=True)
|
||||
pdf_en = models.FileField(upload_to='pdf/en/', blank=True, null=True)
|
||||
|
||||
def __unicode__(self):
|
||||
return u'%s' % self.title
|
||||
|
||||
@property
|
||||
def content(self):
|
||||
return mark_safe(getattr(self, "content_%s" % get_language()) or \
|
||||
self.content_de)
|
||||
|
||||
@property
|
||||
def css_class(self):
|
||||
return CONTENT_CHOICES[self.content_type][1].lower().replace(' ', '_')
|
||||
|
||||
@property
|
||||
def menu_name(self):
|
||||
return getattr(self, "menu_name_%s" % get_language()) or \
|
||||
self.menu_name_de
|
||||
|
||||
@property
|
||||
def pdf_file(self):
|
||||
return getattr(self, "pdf_%s" % get_language()) or self.pdf_de
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return getattr(self, "title_%s" % get_language()) or self.title_de
|
||||
|
||||
def clean(self):
|
||||
if self.parent == None:
|
||||
self.path = self.slug
|
||||
else:
|
||||
self.path = path.join(self.parent.path, self.slug)
|
||||
|
||||
if self.content_type == None:
|
||||
if self.pdf_de:
|
||||
self.content_type = 2
|
||||
if self.content_de:
|
||||
self.content_type = 1
|
||||
else:
|
||||
self.content_type = 0
|
||||
if self.content_type == 1:
|
||||
self.content_de = cleaner.clean_html(self.content_de)
|
||||
self.content_en = cleaner.clean_html(self.content_en)
|
||||
elif self.content_type == 2 and not self.pdf_de.name:
|
||||
raise ValidationError(
|
||||
_(u'Please upload a PDF-File to this PDF-Page.'))
|
||||
|
||||
def get_absolute_url(self):
|
||||
path = '/' + self.path
|
||||
if self.content_type == 1:
|
||||
path += '.html'
|
||||
elif self.content_type == 2:
|
||||
path += '.pdf'
|
||||
else:
|
||||
path += '/'
|
||||
return path
|
||||
|
||||
class Meta(object):
|
||||
ordering = ['parent__id', 'position']
|
||||
unique_together = (('slug', 'parent'),)
|
||||
verbose_name = _('Page')
|
||||
verbose_name_plural = _('Pages')
|
||||
|
||||
|
||||
class Category(ImageModel):
|
||||
name_de = models.CharField(_('Name'), max_length=80)
|
||||
name_en = models.CharField(_('Name'), max_length=80, blank=True)
|
||||
description_de = models.TextField(_('Description'))
|
||||
description_en = models.TextField(_('Description'), blank=True)
|
||||
image = models.ImageField(_('Image'), upload_to='news/categories/',
|
||||
blank=True, null=True)
|
||||
slug = models.SlugField(_('Slug'), unique=True, db_index=True)
|
||||
|
||||
class Meta(object):
|
||||
ordering = ('slug',)
|
||||
verbose_name = _('Category')
|
||||
verbose_name_plural = _('Categories')
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return getattr(self, "name_%s" % get_language(), self.name_de)
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return getattr(self, "description_%s" % get_language(),
|
||||
self.description_de)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('article-archive', kwargs={'category': self.slug})
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
def force_cache_update(sender, instance, **kwargs):
|
||||
for page in instance.subpages.all():
|
||||
page.clean()
|
||||
page.save()
|
||||
cache.delete('all_pages')
|
||||
cache.delete('top_level_pages')
|
||||
|
||||
models.signals.post_delete.connect(force_cache_update, sender=Page)
|
||||
models.signals.post_save.connect(force_cache_update, sender=Page)
|
||||
11
src/content/specs.py
Normal file
11
src/content/specs.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from imagekit.specs import ImageSpec
|
||||
from imagekit import processors
|
||||
|
||||
class ResizeArticle(processors.Resize):
|
||||
width = 210
|
||||
height = 130
|
||||
crop = True
|
||||
|
||||
class Article(ImageSpec):
|
||||
pre_cache = True
|
||||
processors = [ResizeArticle]
|
||||
0
src/content/templatetags/__init__.py
Normal file
0
src/content/templatetags/__init__.py
Normal file
43
src/content/templatetags/fieldset_extras.py
Normal file
43
src/content/templatetags/fieldset_extras.py
Normal file
@@ -0,0 +1,43 @@
|
||||
'''
|
||||
Created on 10.06.2012
|
||||
|
||||
@author: christian
|
||||
'''
|
||||
from django import template
|
||||
from django.utils.datastructures import SortedDict
|
||||
import copy
|
||||
register = template.Library()
|
||||
|
||||
|
||||
class FieldSetNode(template.Node):
|
||||
def __init__(self, form_variable, variable_name, fields):
|
||||
self.fields = fields
|
||||
self.variable_name = variable_name
|
||||
self.form_variable = form_variable
|
||||
|
||||
def render(self, context):
|
||||
form = template.Variable(self.form_variable).resolve(context)
|
||||
new_form = copy.copy(form)
|
||||
# new_form.fields = SortedDict([(key, value) for key, value in
|
||||
# form.fields.items() if key in self.fields])
|
||||
new_form.fields = SortedDict(
|
||||
[(key, form.fields[key]) for key in self.fields]
|
||||
)
|
||||
context[self.variable_name] = new_form
|
||||
|
||||
return u''
|
||||
|
||||
|
||||
def get_fieldset(parser, token):
|
||||
try:
|
||||
_name_, fields, _from_, form, _as_, variable_name = \
|
||||
token.split_contents()
|
||||
fields = fields[1:-1]
|
||||
fields = [field.strip() for field in fields.split(',')]
|
||||
except ValueError:
|
||||
raise template.TemplateSyntaxError(
|
||||
'bad arguments for %r' % token.split_contents()[0]
|
||||
)
|
||||
return FieldSetNode(form, variable_name, fields)
|
||||
|
||||
get_fieldset = register.tag(get_fieldset)
|
||||
16
src/content/tests.py
Normal file
16
src/content/tests.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
This file demonstrates writing tests using the unittest module. These will pass
|
||||
when you run "manage.py test".
|
||||
|
||||
Replace this with more appropriate tests for your application.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class SimpleTest(TestCase):
|
||||
def test_basic_addition(self):
|
||||
"""
|
||||
Tests that 1 + 1 always equals 2.
|
||||
"""
|
||||
self.assertEqual(1 + 1, 2)
|
||||
216
src/content/views.py
Normal file
216
src/content/views.py
Normal file
@@ -0,0 +1,216 @@
|
||||
# Create your views here.
|
||||
from . import models, forms
|
||||
from aggregator.models import Feed
|
||||
from django.conf import settings
|
||||
from django.contrib import comments
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views import generic
|
||||
from utils.mixins import PermissionRequiredMixin
|
||||
import events.models
|
||||
import os
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
|
||||
class ArticleArchiveMixin(object):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(ArticleArchiveMixin, self).get_context_data(**kwargs)
|
||||
context['categories'] = models.Category.objects.all()
|
||||
context['active_category'] = self.category
|
||||
context['feeds'] = Feed.objects.active()
|
||||
return context
|
||||
|
||||
|
||||
class ArticleArchiveIndex(ArticleArchiveMixin, generic.ArchiveIndexView):
|
||||
queryset = models.Article.objects.filter(status=models.STATUS_PUBLISHED)
|
||||
date_field = 'date_created'
|
||||
paginate_by = 5
|
||||
context_object_name = 'article_list'
|
||||
allow_empty = True
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = generic.ArchiveIndexView.get_queryset(self)
|
||||
self.category = self.kwargs.get('category')
|
||||
if self.category:
|
||||
try:
|
||||
self.category = models.Category.objects.get(slug=self.category)
|
||||
queryset = queryset.filter(category=self.category)
|
||||
except models.Category.DoesNotExist:
|
||||
raise Http404(_("This Category does not exist."))
|
||||
return queryset
|
||||
|
||||
|
||||
class ArticleYearArchive(ArticleArchiveMixin, generic.YearArchiveView):
|
||||
queryset = models.Article.objects.filter(status=models.STATUS_PUBLISHED)
|
||||
date_field = 'date_created'
|
||||
paginate_by = 5
|
||||
year_format = '%Y'
|
||||
make_object_list = True
|
||||
allow_empty = True
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = generic.YearArchiveView.get_queryset(self)
|
||||
self.category = self.kwargs.get('category')
|
||||
if self.category:
|
||||
self.category = get_object_or_404(models.Category, slug=self.category)
|
||||
queryset = queryset.filter(category=self.category)
|
||||
return queryset
|
||||
|
||||
|
||||
class ArticleMonthArchive(ArticleArchiveMixin, generic.MonthArchiveView):
|
||||
queryset = models.Article.objects.filter(status=models.STATUS_PUBLISHED)
|
||||
date_field = 'date_created'
|
||||
month_format = '%m'
|
||||
paginate_by = 5
|
||||
make_object_list = True
|
||||
allow_empty = True
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = generic.MonthArchiveView.get_queryset(self)
|
||||
self.category = self.kwargs.get('category')
|
||||
if self.category:
|
||||
self.category = models.Category.objects.get(slug=self.category)
|
||||
queryset = queryset.filter(category=self.category)
|
||||
return queryset
|
||||
|
||||
|
||||
class ArticleDetail(generic.DetailView):
|
||||
queryset = models.Article.objects.filter(status=models.STATUS_PUBLISHED)
|
||||
|
||||
|
||||
class ArticleForm(PermissionRequiredMixin, generic.UpdateView):
|
||||
model = models.Article
|
||||
form_class = forms.ArticleForm
|
||||
permission_required = 'content.change_article'
|
||||
|
||||
def get_object(self):
|
||||
if self.kwargs.get('pk', None):
|
||||
return models.Article.objects.get(pk=self.kwargs['pk'])
|
||||
else:
|
||||
return models.Article(author=self.request.user)
|
||||
|
||||
|
||||
class ImageList(generic.View):
|
||||
def get(self, kwargs):
|
||||
image_list = []
|
||||
response = HttpResponse(content_type='text/javascript')
|
||||
response.write('var tinyMCEImageList = new Array(')
|
||||
os.chdir(settings.MEDIA_ROOT)
|
||||
for dirpath, dirnames, filenames in os.walk('images'): # @UnusedVariable @IgnorePep8
|
||||
filenames.sort()
|
||||
for filename in filenames:
|
||||
image_list.append('["%(name)s", "%(path)s"]' % {
|
||||
'name': os.path.join(dirpath, filename),
|
||||
'path': os.path.join(settings.MEDIA_URL, dirpath, filename)
|
||||
})
|
||||
response.write(', '.join(image_list))
|
||||
response.write(');')
|
||||
return response
|
||||
|
||||
|
||||
class PageAddForm(PermissionRequiredMixin, generic.CreateView):
|
||||
form_class = forms.PageForm
|
||||
template_name = 'content/page_form.html'
|
||||
permission_required = 'content.add_page'
|
||||
|
||||
def get_initial(self):
|
||||
path = os.path.splitext(self.kwargs['path'])[0]
|
||||
if path.startswith('/'):
|
||||
path = path[1:]
|
||||
if path.endswith('/'):
|
||||
path = path[:-1]
|
||||
parent = models.Page.objects.get(path=path)
|
||||
return {'parent': parent}
|
||||
|
||||
|
||||
class PageEditForm(PermissionRequiredMixin, generic.UpdateView):
|
||||
form_class = forms.PageForm
|
||||
permission_required = 'content.change_page'
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
path = os.path.splitext(self.kwargs['path'])[0]
|
||||
if path.startswith('/'):
|
||||
path = path[1:]
|
||||
if path.endswith('/'):
|
||||
path = path[:-1]
|
||||
return models.Page.objects.get(path=path)
|
||||
|
||||
|
||||
class PageHtml(generic.DetailView):
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
try:
|
||||
return models.Page.objects.get(path=self.kwargs['path'],
|
||||
content_type=1)
|
||||
except models.Page.DoesNotExist:
|
||||
raise Http404(
|
||||
_("No Page found matching the Path %s") % self.request.path
|
||||
)
|
||||
|
||||
def get_template_names(self):
|
||||
return self.object.template
|
||||
|
||||
|
||||
class PagePdf(generic.DeleteView):
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
try:
|
||||
return models.Page.objects.get(path=self.kwargs['path'],
|
||||
content_type=2)
|
||||
except models.Page.DoesNotExist:
|
||||
raise Http404(
|
||||
_("No PDF Document found matching the Path %s") %
|
||||
self.request.path
|
||||
)
|
||||
|
||||
def render_to_response(self, context, **response_kwargs):
|
||||
try:
|
||||
pdf_file = open(self.object.pdf_file.path, 'rb')
|
||||
response = HttpResponse(pdf_file.read(),
|
||||
content_type='application/pdf')
|
||||
pdf_file.close()
|
||||
return response
|
||||
except:
|
||||
raise Http404('File not Found %s.pdf' % self.kwargs['path'])
|
||||
|
||||
|
||||
class PageList(generic.View):
|
||||
|
||||
def get(self, kwargs):
|
||||
response = HttpResponse(content_type='text/javascript')
|
||||
response.write('var tinyMCELinkList = new Array(')
|
||||
page_list = []
|
||||
for page in models.Page.objects.filter(status=models.STATUS_PUBLISHED):
|
||||
page_list.append('["%(name)s", "%(path)s"]' % {
|
||||
'name': page.menu_name,
|
||||
'path': page.get_absolute_url()
|
||||
})
|
||||
response.write(', '.join(page_list))
|
||||
response.write(');')
|
||||
return response
|
||||
|
||||
|
||||
class StartPage(generic.TemplateView):
|
||||
template_name = 'index.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
page = models.Page.objects.get(slug='index')
|
||||
random_photo = events.models.Photo.objects.get_random(startpage=True)
|
||||
recent_comment_list = comments.get_model().objects.filter(
|
||||
site__pk=settings.SITE_ID,
|
||||
is_public=True,
|
||||
is_removed=False,
|
||||
)
|
||||
recent_comment_list = recent_comment_list.order_by('-submit_date')[:10]
|
||||
context = {
|
||||
'title': page.title,
|
||||
'content': page.content,
|
||||
'random_photo': random_photo.callout,
|
||||
'current_event': events.models.Event.objects.current_event(),
|
||||
'next_event': events.models.Event.objects.next_event(),
|
||||
'upcoming_events': events.models.Event.objects.upcoming(),
|
||||
'recent_article_list': models.Article.objects.published()[:3],
|
||||
'recent_comment_list': recent_comment_list,
|
||||
}
|
||||
return context
|
||||
queryset = models.Article.objects.filter(status=models.STATUS_PUBLISHED)
|
||||
0
src/events/__init__.py
Normal file
0
src/events/__init__.py
Normal file
49
src/events/admin.py
Normal file
49
src/events/admin.py
Normal file
@@ -0,0 +1,49 @@
|
||||
'''
|
||||
Created on 19.09.2011
|
||||
|
||||
@author: christian
|
||||
'''
|
||||
# import stuff we need from django
|
||||
from django.contrib import admin
|
||||
from events.models import Event, Photo, Location
|
||||
from imagekit.admin import AdminThumbnail
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
|
||||
class EventInline(admin.TabularInline):
|
||||
model = Event
|
||||
fields = ('name', 'start', 'end')
|
||||
verbose_name_plural = _('Event Series')
|
||||
|
||||
|
||||
class EventAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'start', 'end', 'location',)
|
||||
list_editable = ('start', 'end', 'location',)
|
||||
readonly_fields = ('event_series',)
|
||||
date_hierarchy = 'start'
|
||||
search_fields = ('name', 'description')
|
||||
list_per_page = 50
|
||||
inlines = (EventInline,)
|
||||
|
||||
|
||||
class LocationAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'street_address', 'postal_code', 'locality')
|
||||
|
||||
|
||||
class PhotoAdmin(admin.ModelAdmin):
|
||||
admin_thumbnail = AdminThumbnail(image_field='thumbnail')
|
||||
fields = ('image', 'event', 'name', 'description',
|
||||
('anchor_horizontal', 'anchor_vertical'),
|
||||
('photographer', 'created_date'))
|
||||
list_filter = ('event', 'on_startpage',)
|
||||
list_display = ('admin_thumbnail', 'image', 'name', 'event',
|
||||
'photographer', 'on_startpage')
|
||||
list_display_links = ('image',)
|
||||
list_editable = ('on_startpage', 'name', 'event', 'photographer')
|
||||
|
||||
|
||||
# register with CMS
|
||||
|
||||
admin.site.register(Event, EventAdmin)
|
||||
admin.site.register(Photo, PhotoAdmin)
|
||||
admin.site.register(Location, LocationAdmin)
|
||||
50
src/events/forms.py
Normal file
50
src/events/forms.py
Normal file
@@ -0,0 +1,50 @@
|
||||
'''
|
||||
Created on 03.10.2011
|
||||
|
||||
@author: christian
|
||||
'''
|
||||
from . import models
|
||||
from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.translation import ugettext as _
|
||||
from utils.html5.widgets import DateTimeInput
|
||||
|
||||
|
||||
class PhotoUploadForm(forms.Form):
|
||||
error_css_class = 'error'
|
||||
required_css_class = 'required'
|
||||
|
||||
photographer = forms.ModelChoiceField(User.objects.all(), required=True,)
|
||||
event = forms.ModelChoiceField(models.Event.objects.all(), required=True,)
|
||||
upload = forms.FileField(label=_('Images'), required=True,
|
||||
widget=forms.widgets.ClearableFileInput(attrs={'multiple': 'multiple',
|
||||
'accept': "image/gif,image/png,image/jpeg"})
|
||||
)
|
||||
|
||||
|
||||
class EditPhotoForm(forms.ModelForm):
|
||||
error_css_class = 'error'
|
||||
required_css_class = 'required'
|
||||
|
||||
class Meta(object):
|
||||
model = models.Photo
|
||||
fields = ('event', 'name', 'description', 'photographer',
|
||||
'anchor_horizontal', 'anchor_vertical',
|
||||
'created_date', 'on_startpage')
|
||||
|
||||
|
||||
class EventForm(forms.ModelForm):
|
||||
error_css_class = 'error'
|
||||
required_css_class = 'required'
|
||||
|
||||
start = forms.DateTimeField(
|
||||
label=_('start'), required=True,
|
||||
widget=DateTimeInput() # widget=SplitDateTimeWidget()
|
||||
)
|
||||
end = forms.DateTimeField(
|
||||
label=_('end'), required=False,
|
||||
widget=DateTimeInput() # widget=SplitDateTimeWidget()
|
||||
)
|
||||
|
||||
class Meta(object):
|
||||
model = models.Event
|
||||
392
src/events/models.py
Normal file
392
src/events/models.py
Normal file
@@ -0,0 +1,392 @@
|
||||
# -'- Encoding: utf-8 -*-
|
||||
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext as _
|
||||
from imagekit import ImageSpec
|
||||
import imagekit
|
||||
from imagekit.models import ImageSpecField
|
||||
from pilkit import processors
|
||||
import pyexiv2
|
||||
|
||||
from utils import COUNTRIES, OverwriteStorage
|
||||
|
||||
|
||||
CHOICES_HORIZONTAL = (
|
||||
(0.00000001, _('left')),
|
||||
(0.5, _('center')),
|
||||
(1, _('right'))
|
||||
)
|
||||
|
||||
CHOICES_VERTICAL = (
|
||||
(0.00000001, _('top')),
|
||||
(0.5, _('middle')),
|
||||
(1, _('bottom'))
|
||||
)
|
||||
|
||||
|
||||
def get_upload_path(instance, filename):
|
||||
'''
|
||||
Generates the desired file path and filename for an uploaded Image.
|
||||
With this function Django can save the uploaded images to subfolders that
|
||||
also have a meaning for humans.
|
||||
|
||||
@param instance: an Django Object for which the Image has been uploaded.
|
||||
@type instance: a instace of an models.Model sub-class.
|
||||
@param filename: The filename of the uploaded image.
|
||||
@type filename: String
|
||||
'''
|
||||
extension = filename[filename.rfind('.') + 1:]
|
||||
if isinstance(instance, Event):
|
||||
if instance.id:
|
||||
return "events/%s.%s" % (instance.id, extension)
|
||||
else:
|
||||
return "events/%s.%s" % (slugify(instance.name), extension)
|
||||
elif isinstance(instance, Location):
|
||||
if instance.id:
|
||||
return "events/location/%s.%s" % (instance.id, extension)
|
||||
else:
|
||||
return "events/location/%s.%s" % (instance.id, extension)
|
||||
elif isinstance(instance, Photo):
|
||||
return "events/%s/%s" % (instance.event.id, filename)
|
||||
|
||||
|
||||
def post_save_image(sender, instance=None, created=False, raw=False, **kwargs):
|
||||
'''
|
||||
Reganerate the images.
|
||||
'''
|
||||
os.remove(instance.display.path)
|
||||
os.remove(instance.callout.path)
|
||||
os.remove(instance.thumbnail.path)
|
||||
|
||||
"""
|
||||
instance.callout.generate(force=True)
|
||||
instance.display.generate(force=True)
|
||||
instance.thumbnail.generate(force=True)
|
||||
"""
|
||||
|
||||
|
||||
class CalloutImage(ImageSpec):
|
||||
format = 'PNG'
|
||||
width = 620
|
||||
height = 300
|
||||
|
||||
@property
|
||||
def processors(self):
|
||||
model, field_name = imagekit.utils.get_field_info(self.source) # @UnusedVariable @IgnorePep8
|
||||
anchor = model.get_anchor()
|
||||
if anchor:
|
||||
return [processors.Transpose(), processors.ResizeToFill(
|
||||
width=self.width,
|
||||
height=self.height, anchor=anchor
|
||||
)]
|
||||
else:
|
||||
return [processors.Transpose(), processors.SmartResize(
|
||||
width=self.width,
|
||||
height=self.height
|
||||
)]
|
||||
|
||||
|
||||
class DisplayImage(ImageSpec):
|
||||
format = 'PNG'
|
||||
processors = [processors.Transpose(),
|
||||
processors.ResizeToFit(width=940, height=940, upscale=False)]
|
||||
|
||||
|
||||
class ThumbnailImage(CalloutImage):
|
||||
format = 'PNG'
|
||||
width = 140
|
||||
height = 140
|
||||
|
||||
|
||||
imagekit.register.generator('kasu:image:callout', CalloutImage)
|
||||
imagekit.register.generator('kasu:image:display', DisplayImage)
|
||||
imagekit.register.generator('kasu:image:thumbnail', ThumbnailImage)
|
||||
|
||||
|
||||
class ImageModel(models.Model):
|
||||
callout = ImageSpecField(source='image', id='kasu:image:callout')
|
||||
display = ImageSpecField(source='image', id='kasu:image:display')
|
||||
thumbnail = ImageSpecField(source='image', id='kasu:image:thumbnail')
|
||||
|
||||
def get_anchor(self):
|
||||
try:
|
||||
anchor_horizontal = getattr(self, 'anchor_horizontal')
|
||||
anchor_vertical = getattr(self, 'anchor_vertical')
|
||||
except AttributeError:
|
||||
return None
|
||||
if anchor_horizontal and anchor_vertical:
|
||||
return (self.anchor_horizontal, self.anchor_vertical)
|
||||
else:
|
||||
return None
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class EventManager(models.Manager):
|
||||
|
||||
def current_event(self):
|
||||
try:
|
||||
current = self.filter(start__lte=now())
|
||||
current = current.filter(end__gte=now())
|
||||
return current.order_by('start', 'end')[0]
|
||||
except:
|
||||
return None
|
||||
|
||||
def next_event(self):
|
||||
try:
|
||||
return self.filter(start__gt=now()).order_by('start', 'end')[0]
|
||||
except:
|
||||
return None
|
||||
|
||||
def archive(self):
|
||||
return self.filter(start__lt=now())
|
||||
|
||||
def upcoming(self, limit=3):
|
||||
result = self.filter(start__gt=now()).order_by('start', 'end')
|
||||
if limit:
|
||||
return result[1:(limit + 1)]
|
||||
else:
|
||||
return result
|
||||
|
||||
|
||||
class Event(ImageModel):
|
||||
name = models.CharField(_('Name'), max_length=255)
|
||||
description = models.TextField(_("Description"), blank=True)
|
||||
location = models.ForeignKey('Location')
|
||||
start = models.DateTimeField(_('Start'))
|
||||
end = models.DateTimeField(_('End'), blank=True, null=True)
|
||||
url = models.URLField(_('Homepage'), blank=True)
|
||||
image = models.ImageField(_("Image"), upload_to=get_upload_path,
|
||||
storage=OverwriteStorage(), blank=True, null=True)
|
||||
is_tournament = models.BooleanField(_('Tournament'), default=False,
|
||||
help_text=_(u'This event is a tournament, different rules apply for \
|
||||
the kyu ranking.'))
|
||||
photo_count = models.PositiveIntegerField(default=0, editable=False)
|
||||
event_series = models.ForeignKey('Event', blank=True, null=True,
|
||||
on_delete=models.SET_NULL, editable=False,
|
||||
verbose_name=_('Event Series'),
|
||||
help_text=_(u'Wenn dieser Event zu einer Veranstaltungsreihe gehört \
|
||||
werden Ort, Beschreibung, Bild und Homepage von dem hier angegebenen \
|
||||
Event übernommen.'))
|
||||
objects = EventManager()
|
||||
|
||||
class Meta(object):
|
||||
verbose_name = _('Event')
|
||||
verbose_name_plural = _('Events')
|
||||
ordering = ('-start', '-end',)
|
||||
|
||||
def __unicode__(self):
|
||||
try:
|
||||
return "%(name)s (%(date)s)" % {'name': self.name,
|
||||
'date': self.start.date()}
|
||||
except:
|
||||
return "New Event Model"
|
||||
|
||||
def get_absolute_url(self):
|
||||
kwargs = {
|
||||
'pk': self.id,
|
||||
'year': self.start.strftime('%Y'),
|
||||
'month': self.start.strftime('%m')
|
||||
}
|
||||
return reverse('event-detail', kwargs=kwargs)
|
||||
|
||||
def get_edit_url(self):
|
||||
kwargs = {
|
||||
'pk': self.id,
|
||||
'year': self.start.strftime('%Y'),
|
||||
'month': self.start.strftime('%m')
|
||||
}
|
||||
return reverse('event-form', kwargs=kwargs)
|
||||
|
||||
def get_callout(self):
|
||||
if self.image:
|
||||
return self.callout
|
||||
elif self.photo_set.count():
|
||||
return self.photo_set.all().order_by('?')[0].callout
|
||||
elif self.location.image:
|
||||
return self.location.callout
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_thumbnail(self):
|
||||
if self.image:
|
||||
return self.thumbnail
|
||||
elif self.photo_set.count():
|
||||
return self.photo_set.all().order_by('?')[0].thumbnail
|
||||
elif self.location.image:
|
||||
return self.location.thumbnail
|
||||
else:
|
||||
return None
|
||||
|
||||
def save(self, **kwargs):
|
||||
if self.event_series:
|
||||
master_event = self.event_series
|
||||
self.description = master_event.description
|
||||
self.location = master_event.location
|
||||
self.url = master_event.url
|
||||
self.image = master_event.image
|
||||
self.photo_count = self.photo_set.count()
|
||||
models.Model.save(self, **kwargs)
|
||||
|
||||
# Update the rest of the event series:
|
||||
for sub_event in Event.objects.filter(event_series=self):
|
||||
sub_event.save()
|
||||
|
||||
# Update the Hanchans if necesery:
|
||||
for hanchan in self.hanchan_set.all():
|
||||
hanchan.save()
|
||||
|
||||
|
||||
class Location(ImageModel):
|
||||
name = models.CharField(_("Name"), max_length=200)
|
||||
description = models.TextField(_("Description"), blank=True)
|
||||
image = models.ImageField(_("Image"), upload_to=get_upload_path,
|
||||
storage=OverwriteStorage(), blank=True, null=True)
|
||||
url = models.URLField(_('Homepage'), blank=True)
|
||||
postal_code = models.CharField(_('Postal Code'), max_length=6)
|
||||
street_address = models.CharField(_('Street Address'), max_length=127)
|
||||
locality = models.CharField(_('Locality'), max_length=127)
|
||||
country = models.CharField(_('Country'), max_length=2, choices=COUNTRIES)
|
||||
|
||||
class Meta(object):
|
||||
verbose_name = _('Venue')
|
||||
verbose_name_plural = _('Venues')
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def address(self):
|
||||
address = (self.street_address, self.locality, self.country,)
|
||||
return ','.join(address)
|
||||
|
||||
|
||||
class PhotoManager(models.Manager):
|
||||
def get_random(self, startpage=True):
|
||||
if startpage:
|
||||
queryset = self.filter(on_startpage=True)
|
||||
else:
|
||||
queryset = self.all().order_by('?')[0]
|
||||
try:
|
||||
return queryset.order_by('?')[0]
|
||||
except IndexError:
|
||||
return Photo()
|
||||
|
||||
|
||||
class Photo(ImageModel):
|
||||
name = models.CharField(_("Name"), max_length=100, blank=True)
|
||||
image = models.ImageField(_("Image"), upload_to=get_upload_path,
|
||||
storage=OverwriteStorage())
|
||||
anchor_horizontal = models.FloatField(
|
||||
_('horizontal Anchorpoint'),
|
||||
choices=CHOICES_HORIZONTAL,
|
||||
blank=True, null=True,
|
||||
help_text='Der Ankerpunkt ist der interessante Teil des Bildes,\
|
||||
welcher nie abgeschnitten werden darf'
|
||||
)
|
||||
anchor_vertical = models.FloatField(
|
||||
_('vertical Anchorpoint'),
|
||||
choices=CHOICES_VERTICAL,
|
||||
blank=True, null=True,
|
||||
help_text='Wenn kein Ankerpunkt von Hand (horizontal und vertikal)\
|
||||
festgelegt wird, versucht die Software diesen selbst zu erraten.'
|
||||
)
|
||||
|
||||
event = models.ForeignKey(Event)
|
||||
description = models.TextField(_("Description"), max_length=300,
|
||||
blank=True)
|
||||
photographer = models.ForeignKey('auth.User')
|
||||
on_startpage = models.BooleanField(_("Startpage"), default=False,
|
||||
help_text=_('Display this Photo on the Startpage Teaser'))
|
||||
created_date = models.DateTimeField(_("Published on"))
|
||||
views = models.PositiveIntegerField(_("Number of views"), editable=False,
|
||||
default=0)
|
||||
objects = PhotoManager()
|
||||
metadata = None
|
||||
orientation = 1
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Event Image')
|
||||
verbose_name_plural = _('Event Images')
|
||||
ordering = ["created_date"]
|
||||
get_latest_by = "created_date"
|
||||
|
||||
def __unicode__(self):
|
||||
return os.path.basename(self.image.name)
|
||||
|
||||
def read_metadata(self):
|
||||
image_path = os.path.join(settings.MEDIA_ROOT, self.image.name)
|
||||
self.metadata = pyexiv2.ImageMetadata(image_path)
|
||||
self.metadata.read()
|
||||
try:
|
||||
self.orientation = self.metadata['Exif.Image.Orientation'].value
|
||||
except:
|
||||
self.orientation = 1
|
||||
|
||||
def save_metadata(self):
|
||||
if not self.metadata:
|
||||
self.read_metadata()
|
||||
self.metadata['Exif.Image.DateTime'] = self.created_date
|
||||
self.metadata['Exif.Image.ImageDescription'] = self.description
|
||||
self.metadata['Exif.Image.Artist'] = self.photographer.username
|
||||
self.metadata['Exif.Image.Orientation'] = self.orientation or 1
|
||||
self.metadata.write()
|
||||
|
||||
def rotate(self, rotate):
|
||||
'''
|
||||
Sets an the Exif tag in an image to set the right direction.
|
||||
This provides lossless image rotation.
|
||||
@param rotate: 'clockwise' or 'counter-clockwise' the direction in
|
||||
which we should rotate the image in 90° steps.
|
||||
'''
|
||||
if not self.metadata:
|
||||
self.read_metadata()
|
||||
if rotate == 'clockwise':
|
||||
if self.orientation == 1:
|
||||
self.orientation = 6
|
||||
elif self.orientation == 6:
|
||||
self.orientation = 3
|
||||
elif self.orientation == 3:
|
||||
self.orientation = 8
|
||||
else:
|
||||
self.orientation = 1
|
||||
elif rotate == 'counter-clockwise':
|
||||
if self.orientation == 1:
|
||||
self.orientation = 8
|
||||
elif self.orientation == 8:
|
||||
self.orientation = 3
|
||||
elif self.orientation == 3:
|
||||
self.orientation = 6
|
||||
else:
|
||||
self.orientation = 1
|
||||
self.save()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('event-photo', kwargs={'event': self.event.id,
|
||||
'pk': self.id})
|
||||
|
||||
@property
|
||||
def next_photo(self):
|
||||
return self.get_next_by_created_date(event=self.event)
|
||||
|
||||
@property
|
||||
def previous_photo(self):
|
||||
return self.get_previous_by_created_date(event=self.event)
|
||||
|
||||
def save(self, **kwargs):
|
||||
'''
|
||||
Triggers to save related Event to save. This should force an update for
|
||||
the denormalized Photo count.
|
||||
'''
|
||||
ImageModel.save(self, **kwargs)
|
||||
self.save_metadata()
|
||||
self.event.save()
|
||||
|
||||
models.signals.post_save.connect(post_save_image, sender=Photo)
|
||||
40
src/events/specs.py
Normal file
40
src/events/specs.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from imagekit.specs import ImageSpec
|
||||
from imagekit import processors
|
||||
|
||||
class ResizeDisplay(processors.Resize):
|
||||
width = 780
|
||||
crop = False
|
||||
upscale = False
|
||||
|
||||
# first we define our thumbnail resize processor
|
||||
class ResizeCallout(processors.Resize):
|
||||
width = 620
|
||||
height = 300
|
||||
crop = True
|
||||
|
||||
class ResizeAdmin(processors.Resize):
|
||||
width = 60
|
||||
height = 60
|
||||
crop = True
|
||||
|
||||
class ResizeThumbnail(processors.Resize):
|
||||
width = 140
|
||||
height = 140
|
||||
crop = True
|
||||
|
||||
# Different Image Sizes
|
||||
class Admin(ImageSpec):
|
||||
pre_cache = False
|
||||
processors = [processors.Transpose, ResizeAdmin]
|
||||
|
||||
class Display(ImageSpec):
|
||||
pre_cache = False
|
||||
processors = [processors.Transpose, ResizeDisplay]
|
||||
|
||||
class Callout(ImageSpec):
|
||||
pre_cache = False
|
||||
processors = [processors.Transpose, ResizeCallout]
|
||||
|
||||
class Thumbnail(ImageSpec):
|
||||
pre_cache = False
|
||||
processors = [processors.Transpose, ResizeThumbnail]
|
||||
16
src/events/tests.py
Normal file
16
src/events/tests.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
This file demonstrates writing tests using the unittest module. These will pass
|
||||
when you run "manage.py test".
|
||||
|
||||
Replace this with more appropriate tests for your application.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class SimpleTest(TestCase):
|
||||
def test_basic_addition(self):
|
||||
"""
|
||||
Tests that 1 + 1 always equals 2.
|
||||
"""
|
||||
self.assertEqual(1 + 1, 2)
|
||||
228
src/events/views.py
Normal file
228
src/events/views.py
Normal file
@@ -0,0 +1,228 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
# Create your views here.
|
||||
from . import models, forms
|
||||
from datetime import timedelta
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.shortcuts import redirect
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views import generic
|
||||
from icalendar import Calendar, Event
|
||||
import pyexiv2
|
||||
from utils.mixins import PermissionRequiredMixin
|
||||
|
||||
|
||||
class DeleteEventPhoto(generic.DeleteView):
|
||||
model = models.Photo
|
||||
"""
|
||||
def get_object(self, queryset=None):
|
||||
return models.Photo.objects.get(pk=self.kwargs['pk'])
|
||||
"""
|
||||
def get_success_url(self):
|
||||
return reverse('event-photo-list', args=[self.object.event.id])
|
||||
|
||||
@method_decorator(permission_required('events.delete_photo'))
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super(DeleteEventPhoto, self).dispatch(*args, **kwargs)
|
||||
|
||||
class EventArchiveIndex(generic.ArchiveIndexView):
|
||||
allow_empty = True
|
||||
context_object_name = 'event_list'
|
||||
date_field = 'start'
|
||||
model = models.Event
|
||||
paginate_by = 5
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = generic.ArchiveIndexView.get_context_data(self, **kwargs)
|
||||
context['is_archive'] = True
|
||||
return context
|
||||
|
||||
class EventArchiveMonth(generic.MonthArchiveView):
|
||||
date_field = 'start'
|
||||
make_object_list = True
|
||||
model = models.Event
|
||||
month_format = '%m'
|
||||
paginate_by = 15
|
||||
template_name = 'events/event_archive.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = generic.MonthArchiveView.get_context_data(self, **kwargs)
|
||||
context['is_archive'] = True
|
||||
return context
|
||||
|
||||
class EventArchiveYear(generic.YearArchiveView):
|
||||
date_field = 'start'
|
||||
make_object_list = True
|
||||
model = models.Event
|
||||
paginate_by = 15
|
||||
template_name = 'events/event_archive.html'
|
||||
year_format = '%Y'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = generic.YearArchiveView.get_context_data(self, **kwargs)
|
||||
context['is_archive'] = True
|
||||
return context
|
||||
|
||||
class EventDetail(generic.DetailView):
|
||||
model = models.Event
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = generic.DetailView.get_context_data(self, **kwargs)
|
||||
context['form'] = forms.PhotoUploadForm(initial={'event':self.object, 'photographer': self.request.user})
|
||||
return context
|
||||
|
||||
class EventForm(PermissionRequiredMixin, generic.UpdateView):
|
||||
form_class = forms.EventForm
|
||||
permission_required = 'events.add_event'
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
'''
|
||||
If an id has been submitted, try return the existing Event for an update,
|
||||
else creates a new one.
|
||||
@param queryset:
|
||||
'''
|
||||
if self.kwargs.get('pk'):
|
||||
event = models.Event.objects.get(pk=self.kwargs['pk'])
|
||||
if event.event_series:
|
||||
return event.event_series
|
||||
else:
|
||||
return event
|
||||
else:
|
||||
return models.Event()
|
||||
|
||||
class EventGallery(generic.ListView):
|
||||
template_name = 'events/photo_gallery.html'
|
||||
queryset = models.Event.objects.filter(start__lt=timezone.now(), photo_count__gt=0)
|
||||
paginate_by = 12
|
||||
|
||||
class EventListIcal(generic.View):
|
||||
'''
|
||||
Generates an returns an iCal File with all upcoming events.
|
||||
'''
|
||||
|
||||
def add_event(self, event):
|
||||
ics_event = Event()
|
||||
dtstart = timezone.localtime(event.start)
|
||||
dtend = timezone.localtime(event.end)
|
||||
|
||||
ics_event.add('DTSTART', dtstart)
|
||||
ics_event.add('SUMMARY', event.name)
|
||||
ics_event.add('DESCRIPTION', event.description)
|
||||
ics_event.add('LOCATION', event.location.address)
|
||||
ics_event.add('URL', 'http://www.kasu.at' + event.get_absolute_url())
|
||||
ics_event['UID'] = 'event-%d@www.kasu.at' % event.pk
|
||||
ics_event.add('PRIORITY', 5)
|
||||
if event.end:
|
||||
ics_event.add('DTEND', dtend)
|
||||
self.calendar.add_component(ics_event)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
response = HttpResponse(mimetype="text/calendar; charset=UTF-8")
|
||||
self.calendar = Calendar()
|
||||
self.calendar.add('prodid', 'http://www.kasu.at/')
|
||||
self.calendar.add('version', '2.0')
|
||||
for event in models.Event.objects.upcoming(limit=None):
|
||||
self.add_event(event)
|
||||
response.write(self.calendar.to_ical())
|
||||
return response
|
||||
|
||||
class EventPhoto(generic.UpdateView):
|
||||
form_class = forms.EditPhotoForm
|
||||
model = models.Photo
|
||||
template_name = 'events/photo_detail.html'
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if request.POST.get('rotate') and request.user.has_perm('events.change_photo'):
|
||||
photo = models.Photo.objects.get(pk=kwargs['pk'])
|
||||
photo.rotate(request.POST['rotate'])
|
||||
# return redirect(photo.get_absolute_url())
|
||||
return self.get(request)
|
||||
else:
|
||||
return generic.UpdateView.post(self, request, *args, **kwargs)
|
||||
|
||||
class EventPhotoList(generic.ListView):
|
||||
context_object_name = 'photo_list'
|
||||
paginate_by = 36
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = generic.ListView.get_context_data(self, **kwargs)
|
||||
context['event'] = self.event
|
||||
context['form'] = forms.PhotoUploadForm(initial={'event':self.event, 'photographer': self.request.user})
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
try:
|
||||
self.event = models.Event.objects.get(id=self.kwargs['event'])
|
||||
return models.Photo.objects.filter(event=self.event)
|
||||
except models.Event.DoesNotExist:
|
||||
raise Http404(_('Event does not exist'))
|
||||
|
||||
class EventPhotoUpload(generic.FormView):
|
||||
form_class = forms.PhotoUploadForm
|
||||
template_name = 'events/photo_upload.html'
|
||||
|
||||
@method_decorator(permission_required('events.add_photo'))
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super(EventPhotoUpload, self).dispatch(*args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = generic.FormView.get_context_data(self, **kwargs)
|
||||
context['event_list'] = models.Event.objects.archive()[:12]
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
'''
|
||||
Set the current logged in user a default value for the photographer.
|
||||
'''
|
||||
return {
|
||||
'photographer': self.request.user,
|
||||
}
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
'''
|
||||
|
||||
'''
|
||||
self.event = models.Event.objects.get(id=self.request.REQUEST.get('event'))
|
||||
photographer = self.request.POST.get('photographer')
|
||||
if photographer:
|
||||
photographer = User.objects.get(id=photographer)
|
||||
else:
|
||||
photographer = self.request.user
|
||||
|
||||
self.counter = 1
|
||||
for upload in self.request.FILES.getlist('upload'):
|
||||
name = upload.name
|
||||
created_date, description = self.read_exif(upload)
|
||||
photo = models.Photo(
|
||||
event=self.event,
|
||||
photographer=photographer,
|
||||
image=upload,
|
||||
name=name,
|
||||
created_date=created_date,
|
||||
description=description
|
||||
)
|
||||
photo.save()
|
||||
self.counter += 1
|
||||
return redirect('event-photo-list', event=self.event.id)
|
||||
|
||||
def read_exif(self, photo):
|
||||
exif_data = pyexiv2.ImageMetadata.from_buffer(photo.read())
|
||||
exif_data.read()
|
||||
|
||||
try:
|
||||
created_date = exif_data['Exif.Image.DateTime'].value
|
||||
except:
|
||||
created_date = self.event.start + timedelta(minutes=self.counter)
|
||||
try:
|
||||
description = exif_data['Exif.Image.ImageDescription'].value
|
||||
except:
|
||||
description = ''
|
||||
return (created_date, description)
|
||||
|
||||
class UpcomingEvents(generic.ListView):
|
||||
queryset = models.Event.objects.upcoming(limit=None)
|
||||
paginate_by = 12
|
||||
18
src/kasu.wsgi
Normal file
18
src/kasu.wsgi
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/var/www/virtual/kasu.at/virtenv/bin/python
|
||||
import os
|
||||
import sys
|
||||
import site
|
||||
|
||||
src_path = '/var/www/virtual/kasu.at/src'
|
||||
virtpy_path = '/var/www/virtual/kasu.at/virtenv/lib/python2.6/site-packages'
|
||||
|
||||
if virtpy_path not in sys.path:
|
||||
sys.path.insert(1, virtpy_path)
|
||||
if src_path not in sys.path:
|
||||
sys.path.append(src_path)
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
|
||||
|
||||
import django.core.handlers.wsgi
|
||||
application = django.core.handlers.wsgi.WSGIHandler()
|
||||
|
||||
|
||||
BIN
src/locale/de/LC_MESSAGES/django.mo
Normal file
BIN
src/locale/de/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2351
src/locale/de/LC_MESSAGES/django.po
Normal file
2351
src/locale/de/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
src/locale/de_AT/LC_MESSAGES/django.mo
Normal file
BIN
src/locale/de_AT/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2467
src/locale/de_AT/LC_MESSAGES/django.po
Normal file
2467
src/locale/de_AT/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
34
src/logging.conf
Normal file
34
src/logging.conf
Normal file
@@ -0,0 +1,34 @@
|
||||
[loggers]
|
||||
keys=root,doloto.pystories
|
||||
|
||||
[handlers]
|
||||
keys=consoleHandler,rfileHandler
|
||||
|
||||
[formatters]
|
||||
keys=simpleFormatter
|
||||
|
||||
[logger_doloto.pystories]
|
||||
level=DEBUG
|
||||
handlers=consoleHandler,rfileHandler
|
||||
qualname=doloto
|
||||
propagate=0
|
||||
|
||||
[logger_root]
|
||||
level=DEBUG
|
||||
handlers=consoleHandler,rfileHandler
|
||||
|
||||
[handler_consoleHandler]
|
||||
class=StreamHandler
|
||||
level=DEBUG
|
||||
formatter=simpleFormatter
|
||||
args=(sys.stdout,)
|
||||
|
||||
[handler_rfileHandler]
|
||||
class=handlers.RotatingFileHandler
|
||||
level=DEBUG
|
||||
formatter=simpleFormatter
|
||||
args=(%(log_path)s,'a',50000,5)
|
||||
|
||||
[formatter_simpleFormatter]
|
||||
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
|
||||
datefmt=
|
||||
53
src/mahjong_ranking/__init__.py
Normal file
53
src/mahjong_ranking/__init__.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
from django.utils.log import getLogger
|
||||
from django.core.cache import cache
|
||||
|
||||
KYU_RANKS = (
|
||||
(45, 1),
|
||||
(40, 2),
|
||||
(35, 3),
|
||||
(30, 4),
|
||||
(25, 5),
|
||||
(20, 6),
|
||||
(15, 7),
|
||||
(10, 8),
|
||||
(5, 9),
|
||||
(0, 10),
|
||||
)
|
||||
|
||||
DAN_RANKS = (
|
||||
(80, 9),
|
||||
(70, 8),
|
||||
(60, 7),
|
||||
(50, 6),
|
||||
(40, 5),
|
||||
(30, 4),
|
||||
(20, 3),
|
||||
(10, 2),
|
||||
(0, 1),
|
||||
)
|
||||
|
||||
DAN_RANKS_DICT = dict([(dan, points) for points, dan in DAN_RANKS])
|
||||
MIN_HANCHANS_FOR_LADDER = 10
|
||||
|
||||
|
||||
logger = getLogger('kasu.mahjong_ranking')
|
||||
|
||||
|
||||
def set_dirty(event=None, season=None, user=None):
|
||||
if season and user:
|
||||
key_to_add = (season, user)
|
||||
queue_name = 'ladder_ranking_queue'
|
||||
elif season:
|
||||
key_to_add = season
|
||||
queue_name = 'ladder_season_queue'
|
||||
elif event and user:
|
||||
key_to_add = (event, user)
|
||||
queue_name = 'event_ranking_queue'
|
||||
elif user:
|
||||
key_to_add = user
|
||||
queue_name = 'kyu_dan_ranking_queue'
|
||||
|
||||
recalculation_queue = cache.get(queue_name, set())
|
||||
recalculation_queue.add(key_to_add)
|
||||
cache.set(queue_name, recalculation_queue, 360)
|
||||
91
src/mahjong_ranking/admin.py
Normal file
91
src/mahjong_ranking/admin.py
Normal file
@@ -0,0 +1,91 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
'''
|
||||
Created on 19.09.2011
|
||||
|
||||
@author: christian
|
||||
'''
|
||||
# import stuff we need from django
|
||||
from django.contrib import admin
|
||||
from . import models, set_dirty
|
||||
from forms import PlayerFormSet
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
|
||||
def recalculate(modeladmin, request, queryset):
|
||||
if isinstance(modeladmin, HanchanAdmin):
|
||||
for hanchan in queryset:
|
||||
hanchan.save()
|
||||
elif isinstance(modeladmin, EventRankingAdmin):
|
||||
for event_ranking in queryset:
|
||||
set_dirty(event=event_ranking.event_id, user=event_ranking.user_id)
|
||||
elif isinstance(modeladmin, KyuDanAdmin):
|
||||
for kyu_dan_ranking in queryset:
|
||||
set_dirty(user=kyu_dan_ranking.user_id)
|
||||
elif isinstance(modeladmin, LadderSeasonAdmin):
|
||||
for ranking_season in queryset:
|
||||
set_dirty(season=ranking_season.id)
|
||||
elif isinstance(modeladmin, LadderRankingAdmin):
|
||||
for ladder_ranking in queryset:
|
||||
set_dirty(user=ladder_ranking.user_id, season=ladder_ranking.season_id) # @IgnorePep8
|
||||
recalculate.short_description = _("Recalculate")
|
||||
|
||||
|
||||
class PlayerInline(admin.TabularInline):
|
||||
extra = 4
|
||||
formset = PlayerFormSet
|
||||
readonly_fields = ('placement', 'kyu_points', 'dan_points', 'bonus_points',
|
||||
'comment',)
|
||||
max_num = 4
|
||||
model = models.Player
|
||||
|
||||
|
||||
class EventRankingAdmin(admin.ModelAdmin):
|
||||
list_filter = ['event']
|
||||
list_display = ('placement', 'user', 'event', 'avg_placement', 'avg_score',
|
||||
'hanchan_count', 'good_hanchans', 'won_hanchans', 'dirty')
|
||||
list_display_links = ('user',)
|
||||
list_filter = ('event',)
|
||||
actions = [recalculate]
|
||||
|
||||
|
||||
class HanchanAdmin(admin.ModelAdmin):
|
||||
actions = [recalculate]
|
||||
date_hierarchy = 'start'
|
||||
list_filter = ['season', 'event']
|
||||
list_display = ('event', 'start', 'player_names', 'comment',
|
||||
'confirmed', 'valid', 'check_validity')
|
||||
inlines = (PlayerInline,)
|
||||
readonly_fields = ('valid', 'check_validity')
|
||||
|
||||
def save_formset(self, request, form, formset, change):
|
||||
print "Custom save_formset"
|
||||
formset.save()
|
||||
form.is_valid()
|
||||
form.save()
|
||||
|
||||
|
||||
class KyuDanAdmin(admin.ModelAdmin):
|
||||
actions = [recalculate]
|
||||
list_display = ('user', 'kyu', 'kyu_points', 'dan', 'dan_points',
|
||||
'hanchan_count', 'dirty')
|
||||
|
||||
|
||||
class LadderRankingAdmin(admin.ModelAdmin):
|
||||
actions = [recalculate]
|
||||
list_display = ('placement', 'season', 'user', 'avg_placement',
|
||||
'avg_score', 'hanchan_count', 'good_hanchans', 'won_hanchans', 'dirty')
|
||||
list_display_links = ('user',)
|
||||
list_filter = ('season',)
|
||||
|
||||
|
||||
class LadderSeasonAdmin(admin.ModelAdmin):
|
||||
actions = [recalculate]
|
||||
list_display = ('name', 'start', 'end', 'dirty')
|
||||
|
||||
|
||||
admin.site.register(models.EventRanking, EventRankingAdmin)
|
||||
admin.site.register(models.Hanchan, HanchanAdmin)
|
||||
admin.site.register(models.KyuDanRanking, KyuDanAdmin)
|
||||
admin.site.register(models.LadderSeason, LadderSeasonAdmin)
|
||||
admin.site.register(models.LadderRanking, LadderRankingAdmin)
|
||||
112
src/mahjong_ranking/forms.py
Normal file
112
src/mahjong_ranking/forms.py
Normal file
@@ -0,0 +1,112 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
'''
|
||||
Created on 04.10.2011
|
||||
|
||||
@author: christian
|
||||
'''
|
||||
from django.contrib.auth.models import User
|
||||
import django.forms
|
||||
from django.forms.models import BaseInlineFormSet, inlineformset_factory
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from utils.html5 import forms
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
class HanchanForm(forms.ModelForm):
|
||||
error_css_class = 'error'
|
||||
required_css_class = 'required'
|
||||
start = forms.DateTimeField(label=_('start'), required=True)
|
||||
|
||||
class Meta(object):
|
||||
model = models.Hanchan
|
||||
fields = ('event', 'start', 'comment')
|
||||
widgets = {
|
||||
'event': forms.HiddenInput(),
|
||||
'comment': forms.widgets.Textarea(attrs={'rows': 4, 'cols': 40})
|
||||
}
|
||||
|
||||
def clean_start(self):
|
||||
u'''
|
||||
Das Datum darf nicht in der Zukunft liegen und es muss innerhalb der
|
||||
Dauer des Events liegen.
|
||||
'''
|
||||
start = self.cleaned_data['start']
|
||||
event = self.cleaned_data['event']
|
||||
if start > timezone.now():
|
||||
raise django.forms.ValidationError(
|
||||
_("It's not allowed to enter future games."))
|
||||
if not event.start <= start <= event.end:
|
||||
raise django.forms.ValidationError(
|
||||
_("Only games running during this event are allowed."))
|
||||
return start
|
||||
|
||||
|
||||
class HanchanAdminForm(HanchanForm):
|
||||
class Meta(object):
|
||||
model = models.Hanchan
|
||||
fields = ('event', 'start', 'comment', 'confirmed')
|
||||
widgets = {
|
||||
'event': forms.HiddenInput(),
|
||||
'comment': forms.widgets.Textarea(attrs={'rows': 4, 'cols': 40})
|
||||
}
|
||||
|
||||
|
||||
class PlayerForm(forms.ModelForm):
|
||||
error_css_class = 'error'
|
||||
required_css_class = 'required'
|
||||
player_choices = User.objects.filter(groups__in=(1, 2)).distinct()
|
||||
player_choices = player_choices.order_by('username')
|
||||
user = forms.ModelChoiceField(player_choices, required=True)
|
||||
comment = forms.CharField(
|
||||
widget=forms.widgets.TextInput(attrs={'maxlength': 255}),
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PlayerForm, self).__init__(*args, **kwargs)
|
||||
self.fields['bonus_points'].widget.attrs['readonly'] = True
|
||||
self.fields['bonus_points'].widget.attrs['size'] = 2
|
||||
self.fields['comment'].widget.attrs['readonly'] = True
|
||||
self.fields['score'].widget.attrs['size'] = 6
|
||||
self.fields['score'].widget.attrs['type'] = 'number'
|
||||
|
||||
|
||||
class Meta(object):
|
||||
model = models.Player
|
||||
fields = ('hanchan', 'user', 'score', 'bonus_points', 'comment')
|
||||
|
||||
|
||||
class PlayerInlineFormSet(BaseInlineFormSet):
|
||||
|
||||
def clean(self):
|
||||
"""Checks that no two articles have the same title."""
|
||||
for form in self.forms:
|
||||
if form.is_valid() and not form.cleaned_data.get('user'):
|
||||
raise forms.ValidationError(
|
||||
_("A valid Hanchan needs 4 players"))
|
||||
self.validate_unique()
|
||||
return False
|
||||
|
||||
|
||||
class SeasonSelectForm(django.forms.Form):
|
||||
season = django.forms.ChoiceField(label='', choices=('a', 'b', 'c'))
|
||||
|
||||
def __init__(self, user, data=None, *args, **kwargs):
|
||||
super(SeasonSelectForm, self).__init__(args, kwargs)
|
||||
season_list = models.LadderRanking.objects.filter(user=user)
|
||||
season_list = season_list.select_related('user')
|
||||
season_list = season_list.values_list('season__id', 'season__name')
|
||||
self.fields['season'] = django.forms.ChoiceField(choices=season_list)
|
||||
|
||||
PlayerFormSet = inlineformset_factory(
|
||||
models.Hanchan,
|
||||
models.Player,
|
||||
max_num=4,
|
||||
extra=4,
|
||||
form=PlayerForm,
|
||||
formset=PlayerInlineFormSet
|
||||
)
|
||||
1
src/mahjong_ranking/management/__init__.py
Normal file
1
src/mahjong_ranking/management/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
#!/usr/bin/python
|
||||
1
src/mahjong_ranking/management/commands/__init__.py
Normal file
1
src/mahjong_ranking/management/commands/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
#!/usr/bin/python
|
||||
94
src/mahjong_ranking/management/commands/random-ranking.py
Normal file
94
src/mahjong_ranking/management/commands/random-ranking.py
Normal file
@@ -0,0 +1,94 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Generate Randum Mahjong Hanchans to the the Raning System
|
||||
"""
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from events.models import Event
|
||||
from mahjong_ranking import models
|
||||
import random
|
||||
from datetime import timedelta
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Deletes all expired user registrations from the database"
|
||||
|
||||
def add_players(self, hanchan):
|
||||
user_list = set()
|
||||
while len(user_list) < 4:
|
||||
random_user = random.choice(self.user_list)
|
||||
user_list.add(random_user)
|
||||
|
||||
player_list = list()
|
||||
ostwind_list = list()
|
||||
for user in user_list:
|
||||
player_list.append(models.Player(user=user, hanchan=hanchan, score=25000))
|
||||
for player in player_list:
|
||||
player.save()
|
||||
|
||||
end_of_game = False
|
||||
ostwind_list.extend(player_list)
|
||||
ostwind_list.extend(player_list)
|
||||
ostwind = ostwind_list.pop()
|
||||
while not end_of_game:
|
||||
score = random.randrange(1300, 8000, 100)
|
||||
loser = player_list[random.randrange(0,4,1)]
|
||||
winner = player_list[random.randrange(0,4,1)]
|
||||
winner.score += score
|
||||
|
||||
print 'Ostwind: %s, Gewinner: %s, Verlierer: %s, %d Punkte' % (
|
||||
ostwind.user,
|
||||
winner.user,
|
||||
loser.user,
|
||||
score,
|
||||
)
|
||||
|
||||
if winner == loser:
|
||||
# Player wins with Tsumo: Everybody pays a third of the score.
|
||||
print "Tsumo!"
|
||||
for player in player_list:
|
||||
if player != winner:
|
||||
player.score -= score / 3
|
||||
else:
|
||||
loser.score -= score
|
||||
|
||||
for player in player_list:
|
||||
if player.score <= 0:
|
||||
player.score = 0
|
||||
end_of_game = True
|
||||
|
||||
if winner == ostwind:
|
||||
print "Wind bleibt"
|
||||
else:
|
||||
print "Wind wird gewechselt."
|
||||
try:
|
||||
ostwind = ostwind_list.pop()
|
||||
except IndexError:
|
||||
end_of_game = True
|
||||
print '---------------------------------------------------------------------'
|
||||
for player in player_list:
|
||||
print "%s: %s" % (player.user, player.score)
|
||||
player.save()
|
||||
print ""
|
||||
|
||||
def create_hanchan(self, event):
|
||||
start = event.start + timedelta(minutes = random.randrange(00, 300, 15))
|
||||
print event.name, start
|
||||
print '='*80
|
||||
hanchan = models.Hanchan(event=event, start=start)
|
||||
hanchan.save()
|
||||
self.add_players(hanchan)
|
||||
hanchan.save()
|
||||
|
||||
|
||||
def handle(self, *args, **options):
|
||||
num_hanchans = int(options.get('hanchans', 4))
|
||||
self.user_list = list(User.objects.all())
|
||||
|
||||
for event in Event.objects.all():
|
||||
for i in range(random.randrange(2,8)):
|
||||
self.create_hanchan(event)
|
||||
|
||||
|
||||
83
src/mahjong_ranking/middleware.py
Normal file
83
src/mahjong_ranking/middleware.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
Created on 23.05.2011
|
||||
|
||||
@author: christian
|
||||
'''
|
||||
from django.core.cache import cache
|
||||
from django.db import transaction
|
||||
from mahjong_ranking import models
|
||||
from . import logger
|
||||
|
||||
|
||||
class DenormalizationUpdateMiddleware(object):
|
||||
'''
|
||||
This Class deferres the recalculation for the Otaku XP at the end of a
|
||||
response.
|
||||
'''
|
||||
|
||||
def process_response(self, request, response):
|
||||
if request.method != 'POST':
|
||||
# We only do this in POST request, to speedup the responsetime.
|
||||
return response
|
||||
|
||||
# Start an SQL Transaction
|
||||
transaction.enter_transaction_management()
|
||||
transaction.managed(True)
|
||||
|
||||
# recalculate tournament (event) rankings:
|
||||
event_ranking_queue = cache.get('event_ranking_queue', set())
|
||||
if len(event_ranking_queue) > 0:
|
||||
while len(event_ranking_queue) > 0:
|
||||
event_id, user_id = event_ranking_queue.pop()
|
||||
logger.info("recalculate %d tournament Ranking in %s", user_id, event_id)
|
||||
ranking = models.EventRanking.objects.get_or_create(
|
||||
event_id=event_id, user_id=user_id)[0]
|
||||
ranking.recalculate()
|
||||
self.recalculate_event_placement(event_id)
|
||||
cache.set('event_ranking_queue', event_ranking_queue, 360)
|
||||
transaction.commit()
|
||||
|
||||
# recalculate dirty kyu/dan rankings:
|
||||
kyu_dan_ranking_queue = cache.get('kyu_dan_ranking_queue', set())
|
||||
while len(kyu_dan_ranking_queue) > 0:
|
||||
user_id = kyu_dan_ranking_queue.pop()
|
||||
ranking = models.KyuDanRanking.objects.get_or_create(
|
||||
user_id=user_id)[0]
|
||||
ranking.recalculate()
|
||||
cache.set('kyu_dan_ranking_queue', kyu_dan_ranking_queue, 360)
|
||||
transaction.commit()
|
||||
|
||||
# Recaluclate Dirty LadderRankings:
|
||||
ladder_ranking_queue = cache.get('ladder_ranking_queue', set())
|
||||
while len(ladder_ranking_queue) > 0:
|
||||
season_id, user_id = ladder_ranking_queue.pop()
|
||||
if season_id and user_id:
|
||||
ranking = models.LadderRanking.objects.get_or_create(
|
||||
user_id=user_id, season_id=season_id)[0]
|
||||
ranking.recalculate()
|
||||
else:
|
||||
logger.error('Season: %i; User %i - existiert nicht!', season_id, user_id)
|
||||
cache.set('ladder_ranking_queue', ladder_ranking_queue, 360)
|
||||
transaction.commit()
|
||||
|
||||
# Recaluclate Dirty Season Placements:
|
||||
ladder_season_queue = cache.get('ladder_season_queue', set())
|
||||
for season_id in ladder_season_queue:
|
||||
season = models.LadderSeason.objects.get_or_create(pk=season_id)[0]
|
||||
season.recalculate()
|
||||
cache.set('ladder_season_queue', ladder_season_queue, 360)
|
||||
transaction.commit()
|
||||
|
||||
transaction.leave_transaction_management()
|
||||
return response
|
||||
|
||||
def recalculate_event_placement(self, event_id):
|
||||
eventranking_set = models.EventRanking.objects.filter(
|
||||
event_id=event_id).order_by('avg_placement', '-avg_score')
|
||||
eventranking_set.update(placement=None)
|
||||
placement = 1
|
||||
for ranking in eventranking_set:
|
||||
ranking.placement = placement
|
||||
placement += 1
|
||||
ranking.save(force_update=True)
|
||||
602
src/mahjong_ranking/models.py
Normal file
602
src/mahjong_ranking/models.py
Normal file
@@ -0,0 +1,602 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
from datetime import date, timedelta
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
from django.db.models.aggregates import Sum
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext as _
|
||||
from events.models import Event
|
||||
|
||||
from . import KYU_RANKS, DAN_RANKS, DAN_RANKS_DICT, MIN_HANCHANS_FOR_LADDER
|
||||
from . import logger, set_dirty
|
||||
|
||||
kyu_dan_rankings = set()
|
||||
ladder_rankings = set()
|
||||
ladder_seasons = set()
|
||||
|
||||
|
||||
class EventRanking(models.Model):
|
||||
'''
|
||||
Event Rankings funktionieren genauso wie Season Rankings.
|
||||
Sie beschränken sich aber auf einen Event und werden nur dann angestossen,
|
||||
wenn der Event als Turnier markiert wurde.
|
||||
'''
|
||||
user = models.ForeignKey(User)
|
||||
event = models.ForeignKey(Event)
|
||||
placement = models.PositiveIntegerField(blank=True, null=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)
|
||||
dirty = models.BooleanField(default=True, editable=False)
|
||||
|
||||
class Meta(object):
|
||||
ordering = ('placement', 'avg_placement', '-avg_score',)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('event-ranking', args=[self.tourney_id])
|
||||
|
||||
def recalculate(self):
|
||||
'''
|
||||
Berechnet die durschnittliche Platzierung und Punkte, u.v.m. neu.
|
||||
|
||||
Diese Daten werden benötigt um die Platzierung zu erstellen. Sie
|
||||
können zwar sehr leicht errechnet werden, es macht trotzdem Sinn
|
||||
sie zwischen zu speichern.
|
||||
|
||||
Das Eigenschaft dirty ist ein altes Überbleibsel, um das Objekt
|
||||
zur neuberrechnung zu markieren. Mittlerweile wird ein lokaler
|
||||
Cache dafür verwendet, das ist schneller.
|
||||
'''
|
||||
logger.info(u'Recalculate EventRanking for Player %s in %s', self.user, self.event.name) # @IgnorePep8
|
||||
event_hanchans = Player.objects.valid_hanchans(user=self.user_id, event=self.event_id) # @IgnorePep8
|
||||
aggregator = event_hanchans.aggregate(
|
||||
models.Avg('placement'),
|
||||
models.Avg('score'),
|
||||
models.Count('pk'))
|
||||
self.avg_placement = aggregator['placement__avg']
|
||||
self.avg_score = aggregator['score__avg']
|
||||
self.hanchan_count = aggregator['pk__count']
|
||||
self.good_hanchans = event_hanchans.filter(placement__lt=3).count()
|
||||
self.won_hanchans = event_hanchans.filter(placement=1).count()
|
||||
self.dirty = False
|
||||
if self.hanchan_count <= 0:
|
||||
self.delete()
|
||||
else:
|
||||
self.save()
|
||||
|
||||
|
||||
class Hanchan(models.Model):
|
||||
'''
|
||||
Ein komplette Runde Mahjong, die aus genau 4 Spielern bestehen muss.
|
||||
Es werden aber noch andere Tests durchgeführt, ob sie gültig ist.
|
||||
Außerdem gehört jede Hanchan zu einer Veranstaltung.
|
||||
'''
|
||||
comment = models.TextField(_('Comment'), blank=True)
|
||||
confirmed = models.BooleanField(_('Has been Confirmed'), default=True, help_text=_('Only valid and confirmed Hanchans will be counted in the rating.')) # @IgnorePep8
|
||||
event = models.ForeignKey(Event)
|
||||
player_names = models.CharField(max_length=127, editable=False)
|
||||
players = models.ManyToManyField(User, through='Player',verbose_name=_('Players')) # @IgnorePep8
|
||||
season = models.ForeignKey('LadderSeason', blank=True, null=True, editable=False) # @IgnorePep8
|
||||
start = models.DateTimeField(_('Start'), help_text=_('This is crucial to get the right Hanchans that scores')) # @IgnorePep8
|
||||
valid = models.BooleanField(_('Is Valid'), default=False)
|
||||
|
||||
class Meta(object):
|
||||
ordering = ('-start',)
|
||||
verbose_name = _(u'Hanchan')
|
||||
verbose_name_plural = _(u'Hanchans')
|
||||
|
||||
def __str__(self):
|
||||
return "Hanchan am {0:%d.%m.%Y} um {0:%H:%M} ({1})".format(self.start, self.player_names)
|
||||
|
||||
def check_validity(self):
|
||||
'''
|
||||
Prüft ob die Hanchan gültig ist.
|
||||
|
||||
4 Spieler müssen genau 100.000 Punkte erreichen, mehr sind nur erlaubt
|
||||
wenn midestens ein Spieler ins Minus (auf 0) geraten ist. Ansonsten
|
||||
wird die Hanchan als ungültig markiert aber trotzdem abgespeichert,
|
||||
außerdem wird die Begründung zurück gegeben, was nicht gestimmt hat.
|
||||
'''
|
||||
logger.debug("Hanchan wird geprüft ob er valide ist...")
|
||||
if not self.pk:
|
||||
self.valid = False
|
||||
return
|
||||
elif self.player_set.distinct().count() != 4:
|
||||
self.valid = False
|
||||
return _('For a Hanchan exactly 4 players are needed.')
|
||||
|
||||
score_sum = self.player_set.aggregate(Sum('score'))['score__sum']
|
||||
if score_sum == 100000:
|
||||
self.valid = True
|
||||
return '4 Spieler, 100.000 Endpunktestand, die Hanchan ist \
|
||||
korrekt!'
|
||||
elif score_sum > 100000 and self.player_set.filter(score=0):
|
||||
self.valid = True
|
||||
return 'Endpunktestand über 100.000, aber jemand ist auf 0 \
|
||||
gefallen. Die Hanchan stimmt.'
|
||||
elif score_sum < 100000:
|
||||
self.valid = False
|
||||
return 'Endpunktestand weniger als 100.000 Punkte.'
|
||||
elif score_sum > 100000 and not self.player_set.filter(score=0):
|
||||
self.valid = False
|
||||
return 'Endpunktestand über 100.000, aber niemand ist auf 0 \
|
||||
gefallen.'
|
||||
else:
|
||||
self.valid = False
|
||||
return 'Wir wissen nicht warum, aber das kann nicht passen...'
|
||||
|
||||
def clean(self):
|
||||
'''
|
||||
Prüft ob wichtige Vorrausetzungen gegeben sind und aktualisiert ein
|
||||
paar Zwischenspeicher, bevor gespeichert wird.
|
||||
|
||||
Die Hanchan muss 4 (unterschiedliche) Spieler haben, muss in der
|
||||
Veranstaltungzeit liegen, und darf nicht in der Zukunft liegen.
|
||||
Ansonsten wird ein ValidationError ausgegeben und die Hanchan nicht
|
||||
gespeichert.
|
||||
|
||||
Die Gültigkeit wird geprüft und die Sasion in der die Hanchan liegt
|
||||
wird aktualisert.
|
||||
'''
|
||||
logger.debug("Hanchan clean() wurde getriggert!")
|
||||
|
||||
# if self.pk and self.player_set.distinct().count() != 4:
|
||||
# raise ValidationError(
|
||||
# _('For a Hanchan exactly 4 players are needed.'))
|
||||
if self.start and self.start > timezone.now():
|
||||
raise ValidationError(_("It's not allowed to enter future games."))
|
||||
elif not (self.event.start <= self.start <= self.event.end):
|
||||
raise ValidationError(_("Only games during the event are allowed"))
|
||||
return self
|
||||
|
||||
def compute_player_placements(self):
|
||||
u'''
|
||||
Bestimmt die Platzierung eines der Spieler einer Hanchan und speichert
|
||||
diese beim jeweiligen Spieler ab.
|
||||
'''
|
||||
logger.debug("Berechne die Platzierungen neu...")
|
||||
attending_players = self.player_set.select_related('hanchan', 'user')
|
||||
attending_players = attending_players.order_by('-score')
|
||||
other_player_placement = 0
|
||||
other_player_score = 0
|
||||
placement = 1
|
||||
player_list = []
|
||||
logger.info("Compute player pacements for Hanchan Nr. %d", self.pk)
|
||||
for player in attending_players:
|
||||
player_list.append(player.user.username)
|
||||
if player.score <= 0:
|
||||
player.placement = 4
|
||||
elif player.score == other_player_score:
|
||||
player.placement = other_player_placement
|
||||
else:
|
||||
player.placement = placement
|
||||
placement += 1
|
||||
other_player_placement = player.placement
|
||||
other_player_score = player.score
|
||||
player.save(season_id=self.season_id, mark_dirty=True)
|
||||
|
||||
def get_absolute_url(self):
|
||||
'''
|
||||
URL zur Hanchanliste des Events wo diese Hanchan gelistet wurde.
|
||||
'''
|
||||
url = reverse('event-hanchan-list', kwargs={'event': self.event.pk})
|
||||
return u'%(url)s#%(pk)d' % {'url': url, 'pk': self.pk}
|
||||
|
||||
def save(self, **kwargs):
|
||||
logger.debug("Hanchan save() wurde getriggert!")
|
||||
|
||||
self.season = self.season or LadderSeason.objects.get_by_date(self.start)
|
||||
if self.pk:
|
||||
self.check_validity()
|
||||
self.compute_player_placements()
|
||||
self.player_names = ', '.join(self.player_set.values_list(
|
||||
'user__username', flat=True).order_by('-score'))
|
||||
return models.Model.save(self, **kwargs)
|
||||
|
||||
|
||||
class KyuDanRanking(models.Model):
|
||||
u'''
|
||||
Die Einstufung des Spieles im Kyu bzw. Dan System.
|
||||
Im Gegensatz zum Ladder Ranking ist das nicht Saison gebunden.
|
||||
daher läuft es getrennt.
|
||||
'''
|
||||
user = models.OneToOneField(User)
|
||||
dan = models.PositiveSmallIntegerField(blank=True, null=True)
|
||||
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)
|
||||
dirty = models.BooleanField(default=True, editable=False)
|
||||
wins_in_a_row = 0
|
||||
|
||||
class Meta(object):
|
||||
ordering = ('-dan_points', '-kyu_points',)
|
||||
verbose_name = _(u'Kyū/Dan Ranking')
|
||||
verbose_name_plural = _(u'Kyū/Dan Rankings')
|
||||
|
||||
def __unicode__(self):
|
||||
if self.dan_points:
|
||||
return u"%s - %d. Dan" % (self.user.username, self.dan)
|
||||
else:
|
||||
return u"%s - %d. Kyu" % (self.user.username, self.kyu)
|
||||
|
||||
def append_3_in_a_row_bonuspoints(self, hanchan):
|
||||
u'''
|
||||
Wenn der Spieler 3 Siege in folge hatte, bekommt er so viele Punkte
|
||||
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:
|
||||
self.wins_in_a_row += 1
|
||||
else:
|
||||
self.wins_in_a_row = 0
|
||||
|
||||
if self.dan and self.wins_in_a_row > 2:
|
||||
logger.info('adding bonuspoints for 3 wins in a row for %s', self.user) # @IgnorePep8
|
||||
new_dan_rank = self.dan + 1
|
||||
new_dan_points = DAN_RANKS_DICT[new_dan_rank] + 1
|
||||
bonus_points = new_dan_points - self.dan_points
|
||||
|
||||
logger.debug("Stats for %s:", self.user)
|
||||
logger.debug("current dan_points: %d", self.dan_points)
|
||||
logger.debug("current dan: %d", self.dan)
|
||||
logger.debug("min required points for the next dan: %d", new_dan_points) # @IgnorePep8
|
||||
logger.debug("bonus points to add: %d", bonus_points)
|
||||
|
||||
hanchan.dan_points += bonus_points
|
||||
hanchan.bonus_points += bonus_points
|
||||
hanchan.comment += '3 Siege in Folge: +%d Punkte auf den \
|
||||
%d. Dan. ' % (bonus_points, new_dan_rank)
|
||||
self.dan_points += bonus_points
|
||||
|
||||
def append_tournament_bonuspoints(self, player):
|
||||
'''
|
||||
Prüft ob es die letzte Hanchan in einem Turnier war. Wenn ja werden
|
||||
bei Bedarf Bonuspunkte vergeben, falls der Spieler das Turnier gewonnen
|
||||
hat.
|
||||
:param player: Ein Player Objekt
|
||||
'''
|
||||
bonus_points = 0
|
||||
current_event = player.hanchan.event
|
||||
if not current_event.is_tournament:
|
||||
return False # Kein Tunrier, den rest können wir uns sparen.
|
||||
hanchans_this_event = Player.objects.filter(
|
||||
user=self.user, hanchan__event=current_event
|
||||
)
|
||||
hanchans_this_event = hanchans_this_event.order_by('-hanchan__start')
|
||||
last_hanchan_this_event = hanchans_this_event[0].hanchan
|
||||
if player.hanchan != last_hanchan_this_event:
|
||||
return # Das bruacht nur am Ende eines Turnieres gemacht werden.
|
||||
event_ranking = EventRanking.objects.get(
|
||||
user=self.user,
|
||||
event=current_event)
|
||||
if event_ranking.placement == 1:
|
||||
bonus_points += 4
|
||||
player.comment += '+4 Punkte für den Sieg des Turnieres. '
|
||||
if event_ranking.avg_placement == 1:
|
||||
bonus_points += 8
|
||||
player.comment += '+8 Pkt: alle Spiele des Turnieres gewonnen. '
|
||||
player.bonus_points += bonus_points
|
||||
if bonus_points and self.dan:
|
||||
player.dan_points += bonus_points
|
||||
self.dan_points += bonus_points
|
||||
elif bonus_points:
|
||||
player.kyu_points += bonus_points
|
||||
self.kyu_points += bonus_points
|
||||
|
||||
def get_absolute_url(self):
|
||||
if self.dan:
|
||||
return reverse('player-dan-score', args=[self.user.username])
|
||||
else:
|
||||
return reverse('player-kyu-score', args=[self.user.username])
|
||||
|
||||
def recalculate(self):
|
||||
'''
|
||||
Fetches all valid Hanchans from this Player and recalculates his
|
||||
Kyu/Dan Ranking.
|
||||
'''
|
||||
logger.debug("recalculating Kyu/Dan punkte for %s...", self.user)
|
||||
valid_hanchans = Player.objects.valid_hanchans(user=self.user_id)
|
||||
valid_hanchans = valid_hanchans.order_by('hanchan__start')
|
||||
self.kyu_points = 0
|
||||
self.dan_points = 0
|
||||
self.dan = None
|
||||
self.kyu = 10
|
||||
self.hanchan_count = valid_hanchans.count()
|
||||
self.won_hanchans = valid_hanchans.filter(placement=1).count()
|
||||
self.good_hanchans = valid_hanchans.filter(placement=2).count()
|
||||
logger.info("Neuberechnung der Punkte von %s", self.user)
|
||||
|
||||
for hanchan in valid_hanchans:
|
||||
self.update_points(hanchan)
|
||||
self.append_tournament_bonuspoints(hanchan)
|
||||
self.update_rank()
|
||||
self.append_3_in_a_row_bonuspoints(hanchan)
|
||||
self.update_rank()
|
||||
hanchan.save(force_update=True, mark_dirty=False)
|
||||
self.save(force_update=True)
|
||||
|
||||
def update_points(self, player):
|
||||
'''
|
||||
Berechne die Kyu bzw. Dan Punkte für ein Spiel neu.
|
||||
:param player: Das Player Objekt das neuberechnet werden soll.
|
||||
'''
|
||||
player.bonus_points = 0
|
||||
player.comment = ""
|
||||
player.dan_points = None
|
||||
player.kyu_points = None
|
||||
|
||||
if player.hanchan.event.is_tournament:
|
||||
tourney_points = 4 - player.placement
|
||||
if self.dan:
|
||||
player.dan_points = tourney_points
|
||||
else:
|
||||
player.kyu_points = tourney_points
|
||||
elif self.dan:
|
||||
if player.score >= 60000:
|
||||
player.dan_points = 3
|
||||
elif player.score == 0:
|
||||
player.dan_points = -3
|
||||
elif player.placement == 1:
|
||||
player.dan_points = 2
|
||||
elif player.placement == 2:
|
||||
player.dan_points = 1
|
||||
elif player.placement == 3:
|
||||
player.dan_points = -1
|
||||
elif player.placement == 4:
|
||||
player.dan_points = -2
|
||||
elif player.score >= 60000:
|
||||
player.kyu_points = 3
|
||||
elif player.score >= 30000:
|
||||
player.kyu_points = 1
|
||||
elif player.score < 10000:
|
||||
player.kyu_points = -1
|
||||
else:
|
||||
player.kyu_points = 0
|
||||
|
||||
# Add the hanchans points to the players score
|
||||
if player.dan_points:
|
||||
if self.dan_points + player.dan_points < 0:
|
||||
# Only substract so much points that player has 0 Points:
|
||||
player.dan_points -= (self.dan_points + player.dan_points)
|
||||
self.dan_points += player.dan_points
|
||||
elif player.kyu_points:
|
||||
if self.kyu_points + player.kyu_points < 0:
|
||||
# Only substract so much points that player has 0 Points:
|
||||
player.kyu_points -= (self.kyu_points + player.kyu_points)
|
||||
self.kyu_points += player.kyu_points
|
||||
|
||||
def update_rank(self):
|
||||
if self.dan and self.dan_points < 0:
|
||||
self.dan_points = 0
|
||||
self.dan = 1
|
||||
elif self.dan:
|
||||
old_dan = self.dan
|
||||
for min_points, dan_rank in DAN_RANKS:
|
||||
if self.dan_points > min_points:
|
||||
self.dan = dan_rank
|
||||
break
|
||||
if self.dan > old_dan:
|
||||
self.wins_in_a_row = 0
|
||||
elif self.kyu_points < 1:
|
||||
self.kyu_points = 0
|
||||
self.kyu = 10
|
||||
elif self.kyu_points > 50:
|
||||
self.dan = 1
|
||||
self.kyu = None
|
||||
self.dan_points = 0
|
||||
self.kyu_points = 0
|
||||
else:
|
||||
for min_points, kyu_rank in KYU_RANKS:
|
||||
if self.kyu_points > min_points:
|
||||
self.kyu = kyu_rank
|
||||
break
|
||||
|
||||
|
||||
class LadderRanking(models.Model):
|
||||
user = models.ForeignKey(User)
|
||||
season = models.ForeignKey('LadderSeason')
|
||||
placement = models.PositiveIntegerField(blank=True, null=True)
|
||||
avg_placement = models.FloatField(blank=True, null=True)
|
||||
avg_score = models.FloatField(blank=True, null=True)
|
||||
hanchan_count = models.PositiveIntegerField(default=0)
|
||||
good_hanchans = models.PositiveIntegerField(default=0)
|
||||
won_hanchans = models.PositiveIntegerField(default=0)
|
||||
dirty = models.BooleanField(default=True, editable=False)
|
||||
|
||||
class Meta(object):
|
||||
ordering = ('placement', 'avg_placement', '-avg_score',)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('player-ladder-score', args=[self.user.username])
|
||||
|
||||
def recalculate(self):
|
||||
logger.info(u'Recalculate LadderRanking for Player %s in Season %s', self.user, self.season) # @IgnorePep8
|
||||
ladder_hanchans = Player.objects.ladder_hanchans(
|
||||
self.user_id, self.season_id)
|
||||
aggregate = ladder_hanchans.aggregate(
|
||||
models.Avg('placement'),
|
||||
models.Avg('score'),
|
||||
models.Count('pk')
|
||||
)
|
||||
self.dirty = False
|
||||
self.placement = None
|
||||
self.hanchan_count = aggregate['pk__count']
|
||||
self.avg_placement = aggregate['placement__avg']
|
||||
self.avg_score = aggregate['score__avg']
|
||||
self.good_hanchans = ladder_hanchans.filter(placement=2).count()
|
||||
self.won_hanchans = ladder_hanchans.filter(placement=1).count()
|
||||
self.save(force_update=True)
|
||||
|
||||
|
||||
class LadderSeasonManager(models.Manager):
|
||||
|
||||
def current(self):
|
||||
'''
|
||||
Returns the current season and caches the result for 12 hours
|
||||
'''
|
||||
current_season = cache.get('current_mahjong_season')
|
||||
if not current_season:
|
||||
try:
|
||||
today = date.today()
|
||||
current_season = self.filter(start__lte=today, end__gte=today)
|
||||
current_season = current_season[0]
|
||||
except IndexError:
|
||||
current_season = None
|
||||
cache.set('current_mahjong_season', current_season, 4320)
|
||||
return current_season
|
||||
|
||||
def get_by_date(self, deadline):
|
||||
'''
|
||||
returns the season that where running on the given date.
|
||||
:param deadline: the date you're intrested in
|
||||
'''
|
||||
try:
|
||||
season = self.filter(start__lte=deadline, end__gte=deadline)
|
||||
return season[0]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
|
||||
class LadderSeason(models.Model):
|
||||
u'''
|
||||
Eine Saison für das Kasu interne Ladder-Ranking.
|
||||
'''
|
||||
name = models.CharField(max_length=100)
|
||||
start = models.DateField()
|
||||
end = models.DateField()
|
||||
objects = LadderSeasonManager()
|
||||
dirty = models.BooleanField(default=True, editable=False)
|
||||
|
||||
class Meta(object):
|
||||
ordering = ('start',)
|
||||
verbose_name = _('Ladder Season')
|
||||
verbose_name_plural = _('Ladder Seasons')
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def recalculate(self):
|
||||
logger.info(u'Recalculate LadderSeason %s', self.name)
|
||||
self.ladderranking_set.update(placement=None)
|
||||
placement = 1
|
||||
ladder_rankings = self.ladderranking_set.filter(hanchan_count__gt=MIN_HANCHANS_FOR_LADDER) # @IgnorePep8
|
||||
ladder_rankings = ladder_rankings.order_by('avg_placement', '-avg_score') # @IgnorePep8
|
||||
for ranking in ladder_rankings:
|
||||
ranking.placement = placement
|
||||
ranking.save(force_update=True)
|
||||
placement += 1
|
||||
self.dirty = False
|
||||
self.save(force_update=True)
|
||||
|
||||
|
||||
class PlayerManager(models.Manager):
|
||||
use_for_related_fields = True
|
||||
|
||||
def dan_hanchans(self, user=None):
|
||||
dan_hanchans = self.valid_hanchans(user)
|
||||
dan_hanchans = dan_hanchans.filter(dan_points__isnull=False)
|
||||
return dan_hanchans.select_related()
|
||||
|
||||
def kyu_hanchans(self, user=None):
|
||||
queryset = self.valid_hanchans(user).order_by('-hanchan__start')
|
||||
queryset = queryset.filter(kyu_points__isnull=False).select_related()
|
||||
return queryset
|
||||
|
||||
def ladder_hanchans(self, user=None, season=None, num_hanchans=None, max_age=None): # @IgnorePep8
|
||||
queryset = self.valid_hanchans(user).order_by('-hanchan__start')
|
||||
queryset = queryset.select_related()
|
||||
season = season or LadderSeason.objects.current()
|
||||
|
||||
if season:
|
||||
queryset = queryset.filter(hanchan__season_id=season)
|
||||
if user:
|
||||
queryset = queryset.filter(user=user)
|
||||
if max_age:
|
||||
expiration_date = date.today() - timedelta(days=max_age)
|
||||
queryset = queryset.filter(start__gt=expiration_date)
|
||||
if num_hanchans:
|
||||
hanchan_list = queryset.values_list('id', flat=True)[:num_hanchans]
|
||||
hanchan_list = set(hanchan_list)
|
||||
queryset = self.valid_hanchans(user).filter(id__in=hanchan_list)
|
||||
queryset = queryset.select_related().order_by('-hanchan__start')
|
||||
return queryset
|
||||
|
||||
def hanchan_stats(self, queryset=None):
|
||||
queryset = queryset or self.get_query_set()
|
||||
self.num_hanchans = queryset.count()
|
||||
self.won_hanchans = queryset.filter(placement=1).count()
|
||||
self.good_hanchans = queryset.filter(placement__lt=3).count()
|
||||
|
||||
def non_counting_hanchans(self, user=None):
|
||||
queryset = self.exclude(hanchan__valid=True, hanchan__confirmed=True)
|
||||
if user:
|
||||
queryset = queryset.filter(user=user)
|
||||
queryset = queryset.select_related()
|
||||
return queryset.order_by('-hanchan__start')
|
||||
|
||||
def valid_hanchans(self, user=None, event=None):
|
||||
queryset = self.filter(hanchan__valid=True, hanchan__confirmed=True)
|
||||
if user:
|
||||
queryset = queryset.filter(user=user)
|
||||
if event:
|
||||
queryset = queryset.filter(hanchan__event_id=event)
|
||||
queryset = queryset.select_related()
|
||||
return queryset.order_by('-hanchan__start')
|
||||
|
||||
|
||||
class Player(models.Model):
|
||||
hanchan = models.ForeignKey(Hanchan)
|
||||
user = models.ForeignKey(User)
|
||||
score = models.PositiveIntegerField(default=0)
|
||||
placement = models.PositiveSmallIntegerField(blank=True, null=True, default=None) # @IgnorePep8
|
||||
kyu_points = models.PositiveSmallIntegerField(blank=True, null=True, default=None) # @IgnorePep8
|
||||
dan_points = models.PositiveSmallIntegerField(blank=True, null=True, default=None) # @IgnorePep8
|
||||
bonus_points = models.PositiveSmallIntegerField(blank=True, null=True, default=0) # @IgnorePep8
|
||||
comment = models.TextField(_('Comment'), blank=True)
|
||||
objects = PlayerManager()
|
||||
|
||||
class Meta(object):
|
||||
unique_together = ('hanchan', 'user')
|
||||
ordering = ['-score']
|
||||
|
||||
def __str__(self):
|
||||
return "{0}'s Punkte vom {1: %d.%m.%Y um %H:%M}".format(self.user.username, self.hanchan.start)
|
||||
|
||||
def save(self, mark_dirty=True, season_id=None, *args, **kwargs):
|
||||
season_id = season_id or self.hanchan.season_id
|
||||
super(Player, self).save(*args, **kwargs)
|
||||
|
||||
# Set the correct Ladder Ranking to dirty, create it if necessary
|
||||
if mark_dirty:
|
||||
logger.debug("Marking %s's kyu/dan for recalculation", self.user)
|
||||
set_dirty(user=self.user_id)
|
||||
else:
|
||||
return self
|
||||
if season_id:
|
||||
logger.debug("Marking %s's season no. %i ranking for recalculation.", self.user, season_id) # @IgnorePep8
|
||||
set_dirty(season=season_id, user=self.user_id)
|
||||
logger.debug("Marking season no %i for recalculation.", season_id)
|
||||
set_dirty(season=season_id)
|
||||
if self.hanchan.event.is_tournament:
|
||||
logger.debug("Marking tournament %s for recalculation.", self.hanchan.event) # @IgnorePep8
|
||||
set_dirty(event=self.hanchan.event_id, user=self.user_id)
|
||||
return self
|
||||
|
||||
|
||||
def update_ranking_delete(sender, instance, **kwargs): # @UnusedVariable
|
||||
for player in instance.player_set.all():
|
||||
set_dirty(user=player.user_id)
|
||||
if instance.season_id:
|
||||
set_dirty(season=instance.season_id, user=player.user_id)
|
||||
|
||||
models.signals.pre_delete.connect(update_ranking_delete, sender=Hanchan)
|
||||
16
src/mahjong_ranking/tests.py
Normal file
16
src/mahjong_ranking/tests.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
This file demonstrates writing tests using the unittest module. These will pass
|
||||
when you run "manage.py test".
|
||||
|
||||
Replace this with more appropriate tests for your application.
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class SimpleTest(TestCase):
|
||||
def test_basic_addition(self):
|
||||
"""
|
||||
Tests that 1 + 1 always equals 2.
|
||||
"""
|
||||
self.assertEqual(1 + 1, 2)
|
||||
55
src/mahjong_ranking/urls.py
Normal file
55
src/mahjong_ranking/urls.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
'''
|
||||
Created on 03.10.2011
|
||||
|
||||
@author: christian
|
||||
'''
|
||||
from django.conf.urls import * # @UnusedWildImport
|
||||
import views
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^$',
|
||||
views.LadderRankingList.as_view(),
|
||||
name="mahjong-ladder"),
|
||||
url(r'archive/$',
|
||||
views.LadderRankingList.as_view(),
|
||||
kwargs={'is_archive': True},
|
||||
name="mahjong-ladder-archive"),
|
||||
url(r'archive/(?P<season>[\d]+)/$',
|
||||
views.LadderRankingList.as_view(),
|
||||
name="mahjong-ladder-archive"),
|
||||
url(r'players/$',
|
||||
views.KyuDanRankingList.as_view(),
|
||||
name="kyudanranking-list"),
|
||||
url(r'players/(?P<order_by>[\+\-\w]+)/$',
|
||||
views.KyuDanRankingList.as_view(),
|
||||
name="kyudanranking-list"),
|
||||
url(r'event/(?P<event>[\d]+)/$',
|
||||
views.EventHanchanList.as_view(),
|
||||
name="event-hanchan-list"),
|
||||
url(r'event/(?P<event>[\d]+)/ranking/$',
|
||||
views.EventRankingList.as_view(),
|
||||
name="event-ranking"),
|
||||
url(r'event/(?P<event>[\d]+)/add-hanchan/$',
|
||||
views.HanchanForm.as_view(),
|
||||
name="add-hanchan-form"),
|
||||
url(r'hanchan/(?P<hanchan>[\d]+)/edit/$',
|
||||
views.HanchanForm.as_view(),
|
||||
name="edit-hanchan"),
|
||||
url(r'hanchan/(?P<hanchan>[\d]+)/delete/$',
|
||||
views.DeleteHanchan.as_view(),
|
||||
name="delete-hanchan"),
|
||||
url(r'dan_score/(?P<username>[\-\.\d\w]+)/$',
|
||||
views.PlayerDanScore.as_view(),
|
||||
name="player-dan-score"),
|
||||
url(r'invalid_score/(?P<username>[\-\.\d\w]+)/$',
|
||||
views.PlayerInvalidScore.as_view(),
|
||||
name="player-invalid-score"),
|
||||
url(r'kyu_score/(?P<username>[\-\.\d\w]+)/$',
|
||||
views.PlayerKyuScore.as_view(),
|
||||
name="player-kyu-score"),
|
||||
url(r'ladder_score/(?P<username>[\-\.\d\w]+)/$',
|
||||
views.PlayerLadderScore.as_view(),
|
||||
name="player-ladder-score"),
|
||||
)
|
||||
360
src/mahjong_ranking/views.py
Normal file
360
src/mahjong_ranking/views.py
Normal file
@@ -0,0 +1,360 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
from django.contrib import auth, messages
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponseRedirect, Http404
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views import generic
|
||||
from events.models import Event
|
||||
from mahjong_ranking import forms, models
|
||||
from utils.mixins import LoginRequiredMixin, PermissionRequiredMixin
|
||||
import django.forms
|
||||
import xlwt
|
||||
from django import http
|
||||
import urllib
|
||||
from membership.models import Membership
|
||||
|
||||
kyu_dan_order = {
|
||||
'+full_name': ('user__last_name', 'user__first_name'),
|
||||
'-full_name': ('-user__last_name', '-user__first_name'),
|
||||
'+hanchan_count': ('hanchan_count',),
|
||||
'-hanchan_count': ('-hanchan_count',),
|
||||
'+rank': ('-kyu', 'dan'),
|
||||
'-rank': ('-dan', 'kyu'),
|
||||
'+score': ('dan_points', 'kyu_points'),
|
||||
'-score': ('-dan_points', '-kyu_points'),
|
||||
'+username': ('user__username',),
|
||||
'-username': ('-user__username',)
|
||||
}
|
||||
|
||||
|
||||
class DeleteHanchan(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.
|
||||
'''
|
||||
form_class = forms.HanchanForm
|
||||
model = models.Hanchan
|
||||
permission_required = 'mahjong_ranking.delete_hanchan'
|
||||
pk_url_kwarg = 'hanchan'
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('event-hanchan-list',
|
||||
kwargs={'event': self.object.event.pk})
|
||||
|
||||
|
||||
class HanchanForm(PermissionRequiredMixin, generic.UpdateView):
|
||||
'''
|
||||
Ein Formular um eine neue Hanchan anzulegen, bzw. eine bestehende zu
|
||||
bearbeitsen
|
||||
'''
|
||||
form_class = forms.HanchanForm
|
||||
model = models.Hanchan
|
||||
permission_required = 'mahjong_ranking.add_hanchan'
|
||||
|
||||
def form_valid(self, form, formset):
|
||||
if not self.object.pk:
|
||||
self.object = form.save()
|
||||
formset.save()
|
||||
self.object.save()
|
||||
else:
|
||||
formset.save()
|
||||
self.object = form.save()
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
def form_invalid(self, form, formset):
|
||||
return self.render_to_response(self.get_context_data(
|
||||
form=form,
|
||||
formset=formset,
|
||||
object=self.object,
|
||||
))
|
||||
|
||||
def get_form_class(self):
|
||||
"""
|
||||
Returns the form class to use in this view
|
||||
"""
|
||||
if self.request.user.has_perm('mahjong_ranking.delete_hanchan'):
|
||||
return forms.HanchanAdminForm
|
||||
else:
|
||||
return forms.HanchanForm
|
||||
|
||||
def get_formset(self):
|
||||
return forms.PlayerFormSet(
|
||||
data=self.request.POST or None,
|
||||
instance=self.object
|
||||
)
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
if self.kwargs.get('hanchan'):
|
||||
hanchan = models.Hanchan.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'])
|
||||
hanchan = models.Hanchan(
|
||||
event=self.event,
|
||||
start=self.event.start
|
||||
)
|
||||
else:
|
||||
hanchan = self.model()
|
||||
if hanchan.id and not hanchan.valid:
|
||||
messages.warning(self.request, hanchan.check_validity())
|
||||
return hanchan
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = generic.UpdateView.get_context_data(self, **kwargs)
|
||||
context['event'] = self.event
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
'''
|
||||
|
||||
:param request:
|
||||
'''
|
||||
self.object = self.get_object()
|
||||
form = self.get_form(self.get_form_class())
|
||||
formset = self.get_formset()
|
||||
return self.render_to_response(
|
||||
self.get_context_data(form=form, formset=formset)
|
||||
)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
form_class = self.get_form_class()
|
||||
form = self.get_form(form_class)
|
||||
formset = self.get_formset()
|
||||
print form.is_valid()
|
||||
print formset.is_valid()
|
||||
if form.is_valid() and formset.is_valid():
|
||||
return self.form_valid(form, formset)
|
||||
else:
|
||||
return self.form_invalid(form, formset)
|
||||
|
||||
|
||||
class EventHanchanList(generic.ListView):
|
||||
'''
|
||||
Auflistung aller Hanchan die während der Veranstaltung gespielt wurden.
|
||||
'''
|
||||
model = models.Hanchan
|
||||
|
||||
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.prefetch_related('player_set__user')
|
||||
queryset = queryset.order_by('start')
|
||||
return queryset
|
||||
except models.Event.DoesNotExist:
|
||||
raise Http404(_('Event does not exist'))
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = generic.ListView.get_context_data(self, **kwargs)
|
||||
context['event'] = self.event
|
||||
return context
|
||||
|
||||
|
||||
class EventRankingList(generic.ListView):
|
||||
'''
|
||||
Anzeige des Eventrankings, daß erstellt wurde falls der Termin als internes
|
||||
Turnier markiert wurde.
|
||||
'''
|
||||
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)
|
||||
queryset = queryset.prefetch_related()
|
||||
return queryset
|
||||
except models.Event.DoesNotExist:
|
||||
raise Http404(_('Event does not exist'))
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = generic.ListView.get_context_data(self, **kwargs)
|
||||
context['event'] = self.event
|
||||
return context
|
||||
|
||||
|
||||
class KyuDanRankingList(generic.ListView):
|
||||
'''
|
||||
Anzeige aller Spiele mit ihrem Kyu bzw Dan Grad.
|
||||
'''
|
||||
models.KyuDanRanking
|
||||
default_order = '-score'
|
||||
order_by = ''
|
||||
paginate_by = 25
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.order_by = kyu_dan_order[kwargs.get('order_by', self.default_order)] # @IgnorePep8
|
||||
return generic.ListView.dispatch(self, request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = models.KyuDanRanking.objects.all()
|
||||
queryset = queryset.select_related('user__membership')
|
||||
queryset = queryset.order_by(*self.order_by)
|
||||
print self.order_by
|
||||
# print queryset.query
|
||||
return queryset
|
||||
|
||||
|
||||
class LadderRankingList(generic.ListView):
|
||||
model = models.LadderRanking
|
||||
paginate_by = 25
|
||||
season = None
|
||||
is_archive = False
|
||||
|
||||
def get_queryset(self):
|
||||
try:
|
||||
if self.kwargs.get('season'):
|
||||
self.season = models.LadderSeason.objects.get(pk=self.kwargs['season'])
|
||||
self.is_archive = True
|
||||
elif self.kwargs.get('is_archive'):
|
||||
self.season = models.LadderSeason.objects.order_by('-pk')[1]
|
||||
self.is_archive = True
|
||||
else:
|
||||
self.season = models.LadderSeason.objects.current()
|
||||
except models.LadderSeason.DoesNotExist:
|
||||
raise Http404(_('Season does not exist'))
|
||||
queryset = models.LadderRanking.objects.filter(season=self.season, placement__isnull=False).select_related()
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = generic.ListView.get_context_data(self, **kwargs)
|
||||
context['is_archive'] = self.is_archive
|
||||
context['season'] = self.season
|
||||
context['season_archive'] = models.LadderSeason.objects.all()
|
||||
context['latest_hanchan_list'] = models.Hanchan.objects.filter(valid=True)[:5]
|
||||
context['latest_event_list'] = Event.objects.archive()[:5]
|
||||
return context
|
||||
|
||||
|
||||
class LadderRankingExcel(generic.View):
|
||||
center = xlwt.easyxf('align: horiz centre')
|
||||
# Format für Datumswerte
|
||||
date = xlwt.easyxf('align: horiz right', num_format_str='d.MM.YYYY')
|
||||
# Format für Überschriften
|
||||
header = xlwt.easyxf('font: bold on; align: wrap on, vert centre, \
|
||||
horiz center; pattern: back_color grey25;')
|
||||
filename = u"ladderranking.xls"
|
||||
|
||||
def get(self, request, **kwargs):
|
||||
self.queryset = self.team.members.all()
|
||||
response = http.HttpResponse(mimetype=u'application/msexcel')
|
||||
|
||||
filename = urllib.quote(self.filename.encode('utf-8'))
|
||||
response['Content-Disposition'] = "attachment; filename*=UTF-8''%s" % filename
|
||||
|
||||
field_names = [u"Login", u"Name", u"E-Mail", u"Telefon", u"Handy", u"Geburtstag",
|
||||
u"T-Shirt", u"Teams", u"Gr.", u"Kg", u"Notfall Adresse",
|
||||
u"Notfall Nummer", u"Notizen", ]
|
||||
|
||||
|
||||
# Erstelle ein Workbook (Das ist eine Excel Datei)
|
||||
workbook = xlwt.Workbook(encoding='utf-8')
|
||||
|
||||
# Das Sheet ist eine Tabelle in der Excel Datei.
|
||||
# Dort wird die erste Spalte fixiert und mit den Zeilenüberschriften befüllt.
|
||||
sheet = workbook.add_sheet('Mitglieder', cell_overwrite_ok=False)
|
||||
sheet.set_panes_frozen(True)
|
||||
sheet.set_horz_split_pos(1) # in general, freeze after last heading row
|
||||
sheet.set_remove_splits(True) # if user does unfreeze, don't leave a split there
|
||||
for column in range(0, len(field_names)):
|
||||
sheet.write(0, column, field_names[column], style=header)
|
||||
|
||||
# Dann fangen wir an ab der 2. Zeile die einzelnen Datensätze einzuspielen.
|
||||
current_row = 2
|
||||
for user in self.queryset:
|
||||
self.set_col(sheet, current_row, 0, user.username)
|
||||
self.set_col(sheet, current_row, 1, user.get_full_name())
|
||||
self.set_col(sheet, current_row, 2, user.email)
|
||||
try:
|
||||
profile = user.get_profile()
|
||||
self.set_col(sheet, current_row, 3, profile.telephone or None)
|
||||
self.set_col(sheet, current_row, 4, profile.mobilephone or None)
|
||||
self.set_col(sheet, current_row, 5, profile.birthday or None, style=self.date)
|
||||
self.set_col(sheet, current_row, 6, profile.shirt_size or None, style=self.center)
|
||||
self.set_col(sheet, current_row, 7, self.get_other_teams(user))
|
||||
self.set_col(sheet, current_row, 8, profile.height or None)
|
||||
self.set_col(sheet, current_row, 9, profile.weight or None)
|
||||
self.set_col(sheet, current_row, 10, profile.emergency_contact or None)
|
||||
self.set_col(sheet, current_row, 11, profile.emergency_phone or None)
|
||||
except models.MemberProfile.DoesNotExist:
|
||||
pass
|
||||
current_row += 1
|
||||
|
||||
for column in range(0, 13):
|
||||
print column, self.max_colwidth[column]
|
||||
sheet.col(column).width = (self.max_colwidth[column] + 1) * 256
|
||||
|
||||
workbook.save(response)
|
||||
return response
|
||||
|
||||
|
||||
class PlayerScore(LoginRequiredMixin, generic.ListView):
|
||||
paginate_by = 25
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
try:
|
||||
self.user = auth.models.User.objects.get(username=self.kwargs.get('username'))
|
||||
self.membership = Membership.objects.get_or_create(user=self.user)[0]
|
||||
except auth.models.User.DoesNotExist:
|
||||
raise Http404(_("No user found matching the name %s") % self.kwargs.get('username'))
|
||||
return generic.ListView.get(self, request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = generic.ListView.get_context_data(self, **kwargs)
|
||||
context['membership'] = self.membership
|
||||
try:
|
||||
context['kyu_dan_ranking'] = models.KyuDanRanking.objects.get(user=self.user)
|
||||
except:
|
||||
context['ranking'] = None
|
||||
try:
|
||||
context['ladder_ranking'] = models.LadderRanking.objects.get(
|
||||
user=self.user,
|
||||
season=models.LadderSeason.objects.current())
|
||||
except:
|
||||
context['ladder_ranking'] = models.LadderRanking(user=self.user)
|
||||
return context
|
||||
|
||||
|
||||
|
||||
class PlayerDanScore(PlayerScore):
|
||||
template_name = 'mahjong_ranking/player_dan_score.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return models.Player.objects.dan_hanchans(self.user)
|
||||
|
||||
|
||||
class PlayerInvalidScore(PlayerScore):
|
||||
template_name = 'mahjong_ranking/player_invalid_score.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return models.Player.objects.non_counting_hanchans(self.user)
|
||||
|
||||
|
||||
class PlayerKyuScore(PlayerScore):
|
||||
template_name = 'mahjong_ranking/player_kyu_score.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return models.Player.objects.kyu_hanchans(self.user)
|
||||
|
||||
|
||||
class PlayerLadderScore(PlayerScore):
|
||||
template_name = 'mahjong_ranking/player_ladder_score.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = PlayerScore.get_context_data(self, **kwargs)
|
||||
|
||||
season_list = models.LadderRanking.objects.filter(user=self.user).select_related('user')
|
||||
season_list = season_list.values_list('id', 'season__name')
|
||||
|
||||
context['season'] = self.season
|
||||
context['seasons_select_form'] = forms.SeasonSelectForm(user=self.user)
|
||||
context['seasons_select_field'] = django.forms.ChoiceField(choices=season_list)
|
||||
return context
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
if self.request.GET.get('season'):
|
||||
self.season = models.LadderSeason.objects.get(pk=self.request.GET['season'])
|
||||
else:
|
||||
self.season = models.LadderSeason.objects.current()
|
||||
return models.Player.objects.ladder_hanchans(user=self.user, season=self.season)
|
||||
8
src/manage.py
Executable file
8
src/manage.py
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env python2
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
|
||||
from django.core.management import execute_from_command_line
|
||||
execute_from_command_line(sys.argv)
|
||||
1
src/membership/__init__.py
Normal file
1
src/membership/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
PAID_MEMBERSHIP_GROUP = 2
|
||||
62
src/membership/admin.py
Normal file
62
src/membership/admin.py
Normal file
@@ -0,0 +1,62 @@
|
||||
'''
|
||||
Created on 19.09.2011
|
||||
|
||||
@author: christian
|
||||
'''
|
||||
# import stuff we need from django
|
||||
from django.contrib import admin
|
||||
from membership.models import Membership, ActivationRequest
|
||||
from django.utils.translation import ugettext as _
|
||||
from imagekit.admin import AdminThumbnail
|
||||
|
||||
|
||||
def activate_user(modeladmin, request, queryset):
|
||||
for activation in queryset:
|
||||
membership = Membership.objects.get(username=activation.user.username)
|
||||
membership.save()
|
||||
activation.activate()
|
||||
activate_user.short_description = _('Activate selected User')
|
||||
|
||||
|
||||
def cleanup_activation(modeladmin, request, queryset):
|
||||
for activation in queryset:
|
||||
if activation.expired:
|
||||
activation.user.delete()
|
||||
cleanup_activation.short_description = _("Cleanup selected Activation Requests")
|
||||
|
||||
|
||||
class MembershipAdmin(admin.ModelAdmin):
|
||||
admin_thumbnail = AdminThumbnail(image_field='thumbnail')
|
||||
list_filter = ('membership', 'confirmed')
|
||||
list_display = (
|
||||
'admin_thumbnail',
|
||||
'nickname',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'membership',
|
||||
'confirmed',
|
||||
'paid_until',
|
||||
)
|
||||
list_editable = ('confirmed', 'paid_until',)
|
||||
list_display_links = ('nickname',)
|
||||
fieldsets = ((None, {
|
||||
'fields': ('gender', ('first_name', 'last_name'), ('email', 'website'))
|
||||
}),
|
||||
(_('Membership'), {
|
||||
'classes': ('collapse',),
|
||||
'fields': (('membership', 'confirmed'), 'birthday', 'telephone',
|
||||
'street_name', ('post_code', 'city'))
|
||||
}),
|
||||
)
|
||||
ordering = ('nickname',)
|
||||
search_fields = ('nickname', 'first_name', 'last_name',)
|
||||
admin.site.register(Membership, MembershipAdmin)
|
||||
|
||||
|
||||
class RegistrationAdmin(admin.ModelAdmin):
|
||||
list_display = ('username', 'first_name', 'last_name', 'email',
|
||||
'registration_date', 'expired')
|
||||
search_fields = ('user__username', 'user__first_name')
|
||||
actions = [cleanup_activation, activate_user]
|
||||
|
||||
admin.site.register(ActivationRequest, RegistrationAdmin)
|
||||
146
src/membership/forms.py
Normal file
146
src/membership/forms.py
Normal file
@@ -0,0 +1,146 @@
|
||||
'''
|
||||
Created on 03.10.2011
|
||||
|
||||
@author: Christian
|
||||
'''
|
||||
from . import models
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.sites.models import Site
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from utils.html5 import forms
|
||||
from utils.massmailer import MassMailer
|
||||
|
||||
|
||||
class MembershipForm(forms.ModelForm):
|
||||
error_css_class = 'error'
|
||||
required_css_class = 'required'
|
||||
birthday = forms.DateField(label=_('birthday'), required=False)
|
||||
|
||||
class Meta:
|
||||
model = models.Membership
|
||||
fields = (
|
||||
'nickname', 'gender', 'first_name', 'last_name', 'email', 'avatar',
|
||||
'website', 'membership', 'birthday', 'telephone', 'street_name',
|
||||
'post_code', 'city', 'comment'
|
||||
)
|
||||
|
||||
def clean_birthday(self):
|
||||
if self.cleaned_data['membership'] \
|
||||
and not self.cleaned_data['birthday']:
|
||||
raise forms.ValidationError(_('For your membership, we need this. \
|
||||
Please fill out this field yet.'))
|
||||
return self.cleaned_data['birthday']
|
||||
|
||||
def clean_telephone(self):
|
||||
if self.cleaned_data['membership'] \
|
||||
and not self.cleaned_data['telephone']:
|
||||
raise forms.ValidationError(_('For your membership, we need this. \
|
||||
Please fill out this field yet.'))
|
||||
return self.cleaned_data['telephone']
|
||||
|
||||
def clean_street_name(self):
|
||||
if self.cleaned_data['membership'] \
|
||||
and not self.cleaned_data['street_name']:
|
||||
raise forms.ValidationError(_('For your membership, we need this. \
|
||||
Please fill out this field yet.'))
|
||||
return self.cleaned_data['street_name']
|
||||
|
||||
def clean_post_code(self):
|
||||
if self.cleaned_data['membership'] \
|
||||
and not self.cleaned_data['post_code']:
|
||||
raise forms.ValidationError(_('For your membership, we need this. \
|
||||
Please fill out this field yet.'))
|
||||
return self.cleaned_data['post_code']
|
||||
|
||||
def clean_city(self):
|
||||
if self.cleaned_data['membership'] and not self.cleaned_data['city']:
|
||||
raise forms.ValidationError(_('For your membership, we need this. \
|
||||
Please fill out this field yet.'))
|
||||
return self.cleaned_data['city']
|
||||
|
||||
|
||||
class RegistrationForm(forms.ModelForm):
|
||||
"""
|
||||
Form to register a new user account.
|
||||
Validates that the requested username and email is not already in use,
|
||||
requires the password to be entered twice to catch typos.
|
||||
sends an activation request per mail, to validate the eMail.
|
||||
"""
|
||||
error_css_class = 'error'
|
||||
required_css_class = 'required'
|
||||
|
||||
first_name = forms.CharField(max_length=30, required=True,
|
||||
label=_('Given Name'))
|
||||
last_name = forms.CharField(max_length=30, required=True,
|
||||
label=_('Last Name'))
|
||||
username = forms.SlugField(required=True, max_length=30,
|
||||
label=_('Username'),
|
||||
help_text=_("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."))
|
||||
email = forms.EmailField(required=True)
|
||||
password1 = forms.CharField(widget=forms.PasswordInput(),
|
||||
label=_('password'))
|
||||
password2 = forms.CharField(widget=forms.PasswordInput(),
|
||||
label=_('password (again)'))
|
||||
recaptcha = forms.ReCaptchaField()
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('first_name', 'last_name', 'username', 'email',)
|
||||
|
||||
def clean_username(self):
|
||||
'''
|
||||
Validate that the username is not already in use.
|
||||
'''
|
||||
try:
|
||||
User.objects.get(username__iexact=self.cleaned_data['username'])
|
||||
except User.DoesNotExist:
|
||||
return self.cleaned_data['username']
|
||||
raise forms.ValidationError(_(u'This username is already taken. \
|
||||
Please choose another.'))
|
||||
|
||||
def clean_email(self):
|
||||
'''
|
||||
Validate that the supplied email address is unique for the site.
|
||||
'''
|
||||
if User.objects.filter(email__iexact=self.cleaned_data['email']):
|
||||
raise forms.ValidationError(_(u'This email address is already in \
|
||||
use. Please supply a different email address.'))
|
||||
return self.cleaned_data['email']
|
||||
|
||||
def clean_password2(self):
|
||||
password1 = self.cleaned_data.get("password1", "")
|
||||
password2 = self.cleaned_data["password2"]
|
||||
if password1 != password2:
|
||||
raise forms.ValidationError(
|
||||
_("The two password fields didn't match."))
|
||||
return password2
|
||||
|
||||
def send_email(self, activation):
|
||||
mailer = MassMailer()
|
||||
mailer.subject = 'Deine Anmeldung auf %s' % Site.objects.get_current()
|
||||
mailer.txt_template = 'membership/email/activation_email.txt'
|
||||
mailer.context = {
|
||||
'user': activation.user,
|
||||
'site': Site.objects.get_current(),
|
||||
'activation_key': activation.activation_key,
|
||||
'expiration_days': settings.ACCOUNT_ACTIVATION_DAYS,
|
||||
}
|
||||
mailer.add_recipient(activation.user)
|
||||
mailer.send()
|
||||
|
||||
def save(self, commit=True):
|
||||
"""
|
||||
Creates the new ``User`` and ``RegistrationProfile`` and
|
||||
returns the ``User``.
|
||||
"""
|
||||
user = super(RegistrationForm, self).save(commit=False)
|
||||
user.set_password(self.cleaned_data["password1"])
|
||||
user.is_active = False
|
||||
if commit:
|
||||
user.save()
|
||||
activation = models.ActivationRequest.objects.create_pending_registration(user)
|
||||
self.send_email(activation)
|
||||
return user
|
||||
1
src/membership/management/__init__.py
Normal file
1
src/membership/management/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
#!/usr/bin/python
|
||||
1
src/membership/management/commands/__init__.py
Normal file
1
src/membership/management/commands/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
#!/usr/bin/python
|
||||
15
src/membership/management/commands/cleanup-registration.py
Normal file
15
src/membership/management/commands/cleanup-registration.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from membership.models import ActivationRequest
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Delete all expired user registrations from the database"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
|
||||
for activation in ActivationRequest.objects.expired():
|
||||
activation.user.delete()
|
||||
|
||||
55
src/membership/management/commands/get-user.py
Normal file
55
src/membership/management/commands/get-user.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
When you get an error email from your Django app telling you someone got a
|
||||
server error,
|
||||
it’s not always easy to tell which user had a problem.
|
||||
It might help your debugging to know or you might want to contact the user to
|
||||
tell them you have fixed the problem.
|
||||
"""
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.sessions.models import Session
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from optparse import make_option
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
args = '<session_key session_key ...>'
|
||||
help = 'If the session still exists find it and show the related User'
|
||||
option_list = BaseCommand.option_list + (
|
||||
make_option('--delete',
|
||||
action='store_true',
|
||||
dest='delete',
|
||||
default=False,
|
||||
help='Delete the Useraccount'),
|
||||
make_option('--ban',
|
||||
action='store_true',
|
||||
dest='ban',
|
||||
default=False,
|
||||
help='Ban the Useraccount'),
|
||||
)
|
||||
|
||||
|
||||
def handle(self, *args, **options):
|
||||
for session_key in args:
|
||||
try:
|
||||
session = Session.objects.get(session_key=session_key)
|
||||
uid = session.get_decoded().get('_auth_user_id')
|
||||
user = User.objects.get(pk=uid)
|
||||
except Session.DoesNotExist:
|
||||
self.stderr.write('Session "%s" does not exist' % session_key)
|
||||
continue
|
||||
except User.DoesNotExist:
|
||||
self.stderr.write('Session "%s" has no registed User' % session_key)
|
||||
continue
|
||||
if options['delete']:
|
||||
self.stdout.write('deleting %s'% user.username)
|
||||
user.delete()
|
||||
elif options['ban']:
|
||||
self.stdout.write('banning %s' % user.username)
|
||||
user.is_active = False
|
||||
user.save()
|
||||
else:
|
||||
self.stdout.write("Username: %s" % user.username)
|
||||
277
src/membership/models.py
Normal file
277
src/membership/models.py
Normal file
@@ -0,0 +1,277 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
from . import PAID_MEMBERSHIP_GROUP
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import FieldError
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext as _
|
||||
from imagekit.models import ImageSpecField
|
||||
from imagekit.processors import SmartResize
|
||||
from os import path
|
||||
from utils import OverwriteStorage
|
||||
import random
|
||||
import hashlib
|
||||
|
||||
GENDER_CHOICES = (
|
||||
('m', _('Male')),
|
||||
('f', _('Female')),
|
||||
)
|
||||
|
||||
|
||||
def get_upload_path(instance, filename):
|
||||
'''
|
||||
Erstellt den Pfad und Dateinamen für den Upload dynmisch.
|
||||
|
||||
@param instance: The Membership Object for the uploaded image
|
||||
@param filename: the filename of the uploaded image
|
||||
'''
|
||||
extension = path.splitext(filename)[1]
|
||||
return 'membership/%s%s' % (instance.user.username, extension)
|
||||
|
||||
|
||||
class ActivationManager(models.Manager):
|
||||
'''
|
||||
Manages pending user registrations
|
||||
'''
|
||||
|
||||
def activate(self, activation_key):
|
||||
'''
|
||||
searches the pending registrations for the given activation key.
|
||||
Set the corresponding user to active, if the key was found
|
||||
and the key has not expired yet.s
|
||||
|
||||
@param activation_key: the key found in the activation email
|
||||
'''
|
||||
try:
|
||||
activation_request = self.get(activation_key=activation_key)
|
||||
if activation_request.expired():
|
||||
activation_request.user.delete()
|
||||
activation_request.delete()
|
||||
return False
|
||||
elif not activation_request.user.is_active:
|
||||
activation_request.user.is_active = True
|
||||
activation_request.user.save()
|
||||
activation_request.delete()
|
||||
return activation_request.user
|
||||
except self.model.DoesNotExist:
|
||||
return False
|
||||
|
||||
def create_pending_registration(self, user):
|
||||
'''
|
||||
creates a PendingActivation instance with an random activation key.
|
||||
@param user: the user that requests activation.
|
||||
'''
|
||||
salt = str(random.random())
|
||||
activation_key = hashlib.sha1(salt + user.username).hexdigest()
|
||||
|
||||
return self.create(user=user, activation_key=activation_key)
|
||||
|
||||
def expired(self):
|
||||
return self.filter(
|
||||
user__is_active=False,
|
||||
user__date_joined__lt=self.expiration_date
|
||||
)
|
||||
|
||||
|
||||
class ActivationRequest(models.Model):
|
||||
'''
|
||||
Each ActivationRequest contains an activation key and an user.
|
||||
The key will be send by email to the user
|
||||
if the user clicks on the link he can activate his in_active account.
|
||||
'''
|
||||
user = models.ForeignKey(User, unique=True, verbose_name=_('user'))
|
||||
activation_key = models.CharField(_('activation key'), max_length=40)
|
||||
objects = ActivationManager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('pending activation')
|
||||
verbose_name_plural = _('pending activations')
|
||||
|
||||
def __unicode__(self):
|
||||
return _("user registration for %s") % self.user
|
||||
|
||||
def activate(self):
|
||||
self.user.is_active = True
|
||||
self.user.save()
|
||||
self.delete()
|
||||
|
||||
@property
|
||||
def expiration_date(self):
|
||||
timespan = timedelta(days=settings.ACCOUNT_ACTIVATION_DAYS)
|
||||
return self.user.date_joined + timespan
|
||||
|
||||
@property
|
||||
def email(self):
|
||||
return self.user.email
|
||||
|
||||
def expired(self):
|
||||
if self.user.is_active:
|
||||
return False
|
||||
elif timezone.now() >= self.expiration_date:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
expired.boolean = True
|
||||
|
||||
@property
|
||||
def first_name(self):
|
||||
return self.user.first_name
|
||||
|
||||
@property
|
||||
def last_name(self):
|
||||
return self.user.last_name
|
||||
|
||||
@property
|
||||
def registration_date(self):
|
||||
return self.user.date_joined
|
||||
|
||||
@property
|
||||
def username(self):
|
||||
return self.user.username
|
||||
|
||||
|
||||
class MembershipManager(models.Manager):
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
'''
|
||||
First try's to fetch the requested Membership Object from the Database,
|
||||
if the requestetd Membership does not Exists (yet) try to fetch the
|
||||
corresponding User, and create the Membership with the filled in
|
||||
Userdata.
|
||||
'''
|
||||
try:
|
||||
if 'username' in kwargs:
|
||||
return models.Manager.get(self,
|
||||
user__username=kwargs['username']
|
||||
)
|
||||
else:
|
||||
return models.Manager.get(self, *args, **kwargs)
|
||||
except FieldError:
|
||||
user = User.objects.get(*args, **kwargs)
|
||||
except Membership.DoesNotExist:
|
||||
if 'user' in kwargs:
|
||||
user = User.objects.get(pk=kwargs['user'].id)
|
||||
else:
|
||||
user = User.objects.get(*args, **kwargs)
|
||||
|
||||
membership = Membership(
|
||||
user=user,
|
||||
nickname=user.username,
|
||||
first_name=user.first_name,
|
||||
last_name=user.last_name,
|
||||
email=user.email
|
||||
)
|
||||
return membership
|
||||
|
||||
|
||||
class Membership(models.Model):
|
||||
user = models.OneToOneField(User)
|
||||
nickname = models.SlugField(_('Nickname'), unique=True)
|
||||
gender = models.CharField(
|
||||
_("Gender"),
|
||||
max_length=1,
|
||||
choices=GENDER_CHOICES
|
||||
)
|
||||
first_name = models.CharField(_("Given Name"), max_length=30)
|
||||
last_name = models.CharField(_("Last Name"), max_length=30)
|
||||
email = models.EmailField(_('Email'), unique=True)
|
||||
website = models.URLField(blank=True)
|
||||
avatar = models.ImageField(
|
||||
upload_to=get_upload_path,
|
||||
storage=OverwriteStorage(),
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
membership = models.BooleanField(_('Membership'),
|
||||
default=False,
|
||||
help_text=_('Yes, I confirm that I am in agreement with the statutes \
|
||||
and would like to become a member.')
|
||||
)
|
||||
birthday = models.DateField(_("Birthday Date"), blank=True, null=True)
|
||||
telephone = models.CharField(_("Telephone"),
|
||||
max_length=30,
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
street_name = models.CharField(_("Address"),
|
||||
max_length=75,
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
post_code = models.PositiveSmallIntegerField(_("Postcode"),
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
city = models.CharField(_("Town/City"),
|
||||
max_length=75,
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
deposit = models.PositiveSmallIntegerField(default=0, editable=False)
|
||||
registration_date = models.DateField(auto_now_add=True, editable=False)
|
||||
paid_until = models.DateField(_('Paid until'),
|
||||
blank=True,
|
||||
null=True,
|
||||
editable=True
|
||||
)
|
||||
confirmed = models.BooleanField(_('Confirmed'),
|
||||
default=False,
|
||||
help_text=_('This person has paid the membership fee.')
|
||||
)
|
||||
comment = models.TextField(blank=True)
|
||||
objects = MembershipManager()
|
||||
|
||||
thumbnail = ImageSpecField(
|
||||
processors=[SmartResize(width=60, height=60)],
|
||||
format='PNG',
|
||||
source='avatar',
|
||||
)
|
||||
|
||||
profile = ImageSpecField(
|
||||
processors=[SmartResize(width=140, height=140)],
|
||||
format='PNG',
|
||||
source='avatar',
|
||||
)
|
||||
|
||||
class Meta(object):
|
||||
ordering = ('last_name', 'first_name',)
|
||||
verbose_name = _('Membership')
|
||||
verbose_name_plural = _('Memberships')
|
||||
|
||||
def __unicode__(self):
|
||||
return _('Userprofile for %s' % self.user.username)
|
||||
|
||||
def clean(self):
|
||||
|
||||
# Update the Profile Info from the User Object
|
||||
if not self.nickname:
|
||||
self.nickname = self.user.username
|
||||
if not self.first_name:
|
||||
self.first_name = self.user.first_name
|
||||
if not self.last_name:
|
||||
self.last_name = self.user.last_name
|
||||
if not self.email:
|
||||
self.email = self.user.email
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('membership-details',
|
||||
kwargs={'username': self.user.username}
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super(Membership, self).save(*args, **kwargs)
|
||||
self.user.username = self.nickname
|
||||
self.user.first_name = self.first_name
|
||||
self.user.last_name = self.last_name
|
||||
self.user.email = self.email
|
||||
|
||||
if self.confirmed:
|
||||
self.user.groups.add(PAID_MEMBERSHIP_GROUP)
|
||||
else:
|
||||
self.user.groups.remove(PAID_MEMBERSHIP_GROUP)
|
||||
|
||||
self.user.save()
|
||||
21
src/membership/specs.py
Normal file
21
src/membership/specs.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from imagekit.specs import ImageSpec
|
||||
from imagekit import processors
|
||||
|
||||
|
||||
class ResizeThumbnail(processors.Resize):
|
||||
width = 60
|
||||
height = 60
|
||||
crop = True
|
||||
|
||||
class ResizeProfile(processors.Resize):
|
||||
width = 140
|
||||
height = 140
|
||||
crop = True
|
||||
|
||||
class Thumbnail(ImageSpec):
|
||||
pre_cache = True
|
||||
processors = (ResizeThumbnail,)
|
||||
|
||||
class Profile(ImageSpec):
|
||||
pre_cache = False
|
||||
processors = (ResizeProfile,)
|
||||
46
src/membership/urls.py
Normal file
46
src/membership/urls.py
Normal file
@@ -0,0 +1,46 @@
|
||||
'''
|
||||
Created on 03.10.2011
|
||||
|
||||
@author: christian
|
||||
'''
|
||||
from django.conf.urls import * # @UnusedWildImport
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url('', include('social.apps.django_app.urls', namespace='social')),
|
||||
url('', include('django.contrib.auth.urls')),
|
||||
url(r'^register/$',
|
||||
views.RegisterForm.as_view(),
|
||||
name="membership-register"),
|
||||
url(r'^activate/(?P<activation_key>[\d\w]+)/$',
|
||||
views.ActivateRegistration.as_view(),
|
||||
name='membership-activate-registration'),
|
||||
url(r'^activation_sent/$',
|
||||
views.ActivationSent.as_view(),
|
||||
name="membership-registration-complete"),
|
||||
url(r'^(?P<username>[\-\.\d\w]+)/$',
|
||||
views.MembershipDetail.as_view(),
|
||||
name='membership-details'),
|
||||
url(r'^(?P<username>[\-\.\d\w]+)/edit/$',
|
||||
views.EditMembership.as_view(),
|
||||
name="membership-edit")
|
||||
)
|
||||
"""
|
||||
urlpatterns += patterns('social_auth.views',
|
||||
url(r'^login/(?P<backend>[^/]+)/$', 'auth',
|
||||
name='socialauth_begin'),
|
||||
url(r'^complete/(?P<backend>[^/]+)/$', 'complete',
|
||||
name='socialauth_complete'),
|
||||
url(r'^associate/(?P<backend>[^/]+)/$', 'auth',
|
||||
name='socialauth_associate_begin'),
|
||||
url(r'^associate/complete/(?P<backend>[^/]+)/$', 'complete',
|
||||
name='socialauth_associate_complete'),
|
||||
url(r'^disconnect/(?P<backend>[^/]+)/$', 'disconnect',
|
||||
name='socialauth_disconnect'),
|
||||
url(r'^disconnect/(?P<backend>[^/]+)/(?P<association_id>[^/]+)/$',
|
||||
'disconnect',
|
||||
name='socialauth_disconnect_individual'),
|
||||
)
|
||||
"""
|
||||
109
src/membership/views.py
Normal file
109
src/membership/views.py
Normal file
@@ -0,0 +1,109 @@
|
||||
from django import http
|
||||
from django.conf import settings
|
||||
from django.contrib import auth, messages
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views import generic
|
||||
from mahjong_ranking.models import KyuDanRanking, LadderRanking, LadderSeason
|
||||
import forms
|
||||
import models
|
||||
from utils import mixins
|
||||
|
||||
|
||||
class ActivateRegistration(generic.DetailView):
|
||||
'''
|
||||
Activates the Registration of an User and logs him in
|
||||
'''
|
||||
template_name = 'membership/activation_error.html'
|
||||
|
||||
def get(self, request, **kwargs):
|
||||
activation_key = self.kwargs.get('activation_key')
|
||||
user = self.get_user(activation_key)
|
||||
print 'User: ', user
|
||||
if user:
|
||||
return self.login(user)
|
||||
else:
|
||||
self.object = user
|
||||
context = self.get_context_data()
|
||||
return self.render_to_response(context)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = generic.DetailView.get_context_data(self, **kwargs)
|
||||
context.update({
|
||||
'expiration_days': settings.ACCOUNT_ACTIVATION_DAYS,
|
||||
'activation_key': self.kwargs.get('activation_key'),
|
||||
|
||||
})
|
||||
return context
|
||||
|
||||
def get_user(self, activation_key):
|
||||
user = models.ActivationRequest.objects.activate(activation_key)
|
||||
if user:
|
||||
return user
|
||||
elif self.request.user.is_authenticated():
|
||||
return self.request.user
|
||||
else:
|
||||
return None
|
||||
|
||||
def login(self, user):
|
||||
backend = auth.get_backends()[0]
|
||||
user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__) # @IgnorePep8
|
||||
auth.login(self.request, user)
|
||||
messages.success(self.request, _('Activation successful. \
|
||||
You can now login anytime with you username and password.'))
|
||||
redirect_url = reverse('membership-edit', args=(user.username,))
|
||||
return http.HttpResponseRedirect(redirect_url) # @IgnorePep8
|
||||
|
||||
|
||||
class ActivationSent(generic.TemplateView):
|
||||
template_name = 'membership/activation_sent.html'
|
||||
|
||||
|
||||
class EditMembership(mixins.LoginRequiredMixin, generic.UpdateView):
|
||||
form_class = forms.MembershipForm
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
if self.request.user.has_perm('membership.change_membership'):
|
||||
return models.Membership.objects.get(
|
||||
username=self.kwargs['username'])
|
||||
else:
|
||||
return models.Membership.objects.get(user=self.request.user)
|
||||
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, _('User Profile changed successfully'))
|
||||
return generic.UpdateView.form_valid(self, form)
|
||||
|
||||
|
||||
class MembershipDetail(mixins.LoginRequiredMixin, generic.DetailView):
|
||||
model = models.Membership
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
if self.kwargs.get('username'):
|
||||
return models.Membership.objects.get(username=self.kwargs['username'])
|
||||
elif self.request.user.is_authenticated():
|
||||
return models.Membership.objects.get(user=self.request.user)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = generic.DetailView.get_context_data(self, **kwargs)
|
||||
try:
|
||||
context['kyu_dan_ranking'] = KyuDanRanking.objects.get(
|
||||
user_id=self.object.user_id)
|
||||
except:
|
||||
context['kyu_dan_ranking'] = None
|
||||
try:
|
||||
context['ladder_ranking'] = LadderRanking.objects.get(
|
||||
user_id=self.object.user_id,
|
||||
season=LadderSeason.objects.current())
|
||||
except:
|
||||
context['ladder_ranking'] = LadderRanking(user=self.object.user)
|
||||
return context
|
||||
|
||||
|
||||
class RegisterForm(generic.FormView):
|
||||
form_class = forms.RegistrationForm
|
||||
success_url = '/membership/activation_sent/'
|
||||
template_name = 'membership/register_form.html'
|
||||
|
||||
def form_valid(self, form):
|
||||
form.save()
|
||||
return generic.FormView.form_valid(self, form)
|
||||
2563
src/output.txt
Normal file
2563
src/output.txt
Normal file
File diff suppressed because it is too large
Load Diff
285
src/settings.py
Normal file
285
src/settings.py
Normal file
@@ -0,0 +1,285 @@
|
||||
from os import path
|
||||
from django.template.defaultfilters import slugify
|
||||
|
||||
gettext = lambda s: s
|
||||
|
||||
DEBUG = False
|
||||
TEMPLATE_DEBUG = DEBUG
|
||||
PROJECT_PATH = path.abspath(path.join(path.dirname(__file__), '..'))
|
||||
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'grappelli',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.admindocs',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.comments',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.sites',
|
||||
'django.contrib.staticfiles',
|
||||
'django_markdown',
|
||||
'aggregator',
|
||||
'content',
|
||||
'events',
|
||||
'imagekit',
|
||||
'membership',
|
||||
'social.apps.django_app.default',
|
||||
'mahjong_ranking',
|
||||
]
|
||||
|
||||
ALLOWED_HOSTS = ['localhost', '.kasu.at']
|
||||
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_HOST = "smtp.googlemail.com"
|
||||
EMAIL_PORT = "587"
|
||||
EMAIL_HOST_USER = "webmaster@kasu.at"
|
||||
EMAIL_HOST_PASSWORD = "Ees6aang"
|
||||
EMAIL_USE_TLS = True
|
||||
DEFAULT_FROM_EMAIL = "webmaster@kasu.at"
|
||||
|
||||
|
||||
LOCALE_PATHS = (path.join(PROJECT_PATH, 'src', 'locale/'),)
|
||||
AUTH_PROFILE_MODULE = 'membership.Membership'
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'social.backends.facebook.FacebookOAuth2',
|
||||
'social.backends.google.GoogleOAuth2',
|
||||
'social.backends.twitter.TwitterOAuth',
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
)
|
||||
|
||||
ADMINS = (
|
||||
('Christian Berg', 'xeniac.at@gmail.com'),
|
||||
)
|
||||
|
||||
MANAGERS = ADMINS
|
||||
ACCOUNT_ACTIVATION_DAYS = 5
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'NAME': 'kasu',
|
||||
'USER': 'kasu',
|
||||
'PASSWORD': 'F9lFbUaUvMcDA',
|
||||
'HOST': 'localhost',
|
||||
},
|
||||
}
|
||||
|
||||
# Local time zone for this installation. Choices can be found here:
|
||||
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
|
||||
# although not all choices may be available on all operating systems.
|
||||
# On Unix systems, a value of None will cause Django to use the same
|
||||
# timezone as the operating system.
|
||||
# If running in a Windows environment this must be set to the same as your
|
||||
# system time zone.
|
||||
USE_TZ = True
|
||||
TIME_ZONE = 'Europe/Vienna'
|
||||
|
||||
# Language code for this installation. All choices can be found here:
|
||||
# http://www.i18nguy.com/unicode/language-identifiers.html
|
||||
LANGUAGE_CODE = 'de-at'
|
||||
LANGUAGES = (
|
||||
('de', gettext('German')),
|
||||
('en', gettext("English")),
|
||||
)
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
# If you set this to False, Django will make some optimizations so as not
|
||||
# to load the internationalization machinery.
|
||||
USE_I18N = True
|
||||
|
||||
# If you set this to False, Django will not format dates, numbers and
|
||||
# calendars according to the current locale
|
||||
USE_L10N = True
|
||||
|
||||
# Absolute filesystem path to the directory that will hold user-uploaded files.
|
||||
# Example: "/home/media/media.lawrence.com/media/"
|
||||
MEDIA_ROOT = path.join(PROJECT_PATH, 'htdocs', 'media')
|
||||
|
||||
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
|
||||
# trailing slash.
|
||||
# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
|
||||
MEDIA_URL = '/media/'
|
||||
|
||||
|
||||
# Absolute path to the directory static files should be collected to.
|
||||
# Don't put anything in this directory yourself; store your static files
|
||||
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
|
||||
# Example: "/home/media/media.lawrence.com/static/"
|
||||
STATIC_ROOT = path.join(PROJECT_PATH, 'htdocs')
|
||||
|
||||
# URL prefix for static files.
|
||||
# Example: "http://media.lawrence.com/static/"
|
||||
STATIC_URL = '/static/'
|
||||
# ADMIN_MEDIA_PREFIX = path.join(STATIC_URL, "grappelli/")
|
||||
|
||||
# Additional locations of static files
|
||||
STATICFILES_DIRS = (
|
||||
# Put strings here, like "/home/html/static" or "C:/www/django/static".
|
||||
# Always use forward slashes, even on Windows.
|
||||
# Don't forget to use absolute paths, not relative paths.
|
||||
path.join(PROJECT_PATH, 'static'),
|
||||
)
|
||||
|
||||
# List of finder classes that know how to find static files in
|
||||
# various locations.
|
||||
STATICFILES_FINDERS = (
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
)
|
||||
# Make this unique, and don't share it with anybody.
|
||||
SECRET_KEY = 'uve7*2z2+2fs!ts80e(^2kxo^960!hl)cns@fpt_e%7qg52wy0'
|
||||
|
||||
MIDDLEWARE_CLASSES = [
|
||||
'mahjong_ranking.middleware.DenormalizationUpdateMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'urls'
|
||||
|
||||
# List of callables that know how to import templates from various sources.
|
||||
TEMPLATE_LOADERS = (
|
||||
'django.template.loaders.filesystem.Loader',
|
||||
'django.template.loaders.app_directories.Loader',
|
||||
)
|
||||
|
||||
TEMPLATE_DIRS = (
|
||||
path.join(PROJECT_PATH, 'templates/'),
|
||||
)
|
||||
|
||||
TEMPLATE_CONTEXT_PROCESSORS = (
|
||||
'django.core.context_processors.request',
|
||||
'django.core.context_processors.csrf',
|
||||
'django.core.context_processors.debug',
|
||||
'django.core.context_processors.i18n',
|
||||
'django.core.context_processors.media',
|
||||
'django.core.context_processors.request',
|
||||
'django.core.context_processors.static',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
'content.context_processors.content_menus',
|
||||
'social.apps.django_app.context_processors.backends',
|
||||
'social.apps.django_app.context_processors.login_redirect',
|
||||
)
|
||||
|
||||
# PREFIX_DEFAULT_LOCALE = True
|
||||
|
||||
# A sample logging configuration. The only tangible logging
|
||||
# performed by this configuration is to send an email to
|
||||
# the site admins on every HTTP 500 error.
|
||||
# See http://docs.djangoproject.com/en/dev/topics/logging for
|
||||
# more details on how to customize your logging configuration.
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': True,
|
||||
'filters': {
|
||||
'require_debug_false': {
|
||||
'()': 'django.utils.log.RequireDebugFalse'
|
||||
}
|
||||
},
|
||||
'formatters': {
|
||||
'verbose': {
|
||||
'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' # @IgnorePep8
|
||||
},
|
||||
'simple': {
|
||||
'format': '%(levelname)s %(message)s'
|
||||
},
|
||||
},
|
||||
'handlers': {
|
||||
'null': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'django.utils.log.NullHandler',
|
||||
},
|
||||
'console': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'simple'
|
||||
},
|
||||
'mail_admins': {
|
||||
'level': 'ERROR',
|
||||
'filters': ['require_debug_false'],
|
||||
'class': 'django.utils.log.AdminEmailHandler',
|
||||
}
|
||||
},
|
||||
'loggers': {
|
||||
'django': {
|
||||
'handlers': ['console'],
|
||||
'level': 'INFO',
|
||||
'propagate': True,
|
||||
},
|
||||
'django.request': {
|
||||
'handlers': ['console', 'mail_admins'],
|
||||
'level': 'ERROR',
|
||||
'propagate': False,
|
||||
},
|
||||
'kasu': {
|
||||
'handlers': ['console', 'mail_admins'],
|
||||
'level': 'DEBUG',
|
||||
'propagate': False,
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
}
|
||||
}
|
||||
|
||||
RECAPTCHA_PUBLIC_KEY = '6LcX_dcSAAAAAKx9FCP1Nc3EZMmiFIUPeUv5CVnt'
|
||||
RECAPTCHA_PRIVATE_KEY = '6LcX_dcSAAAAAHL9m05oLSl-PiU71MLdheFWJnrC'
|
||||
RECAPTCHA_THEME = 'red'
|
||||
INTERNAL_IPS = ('127.0.0.1', '192.168.1.8')
|
||||
|
||||
|
||||
##################
|
||||
# SOCIAL AUTH #
|
||||
##################
|
||||
|
||||
SOCIAL_AUTH_ENABLED_BACKENDS = ('google-oauth2', 'twitter', 'facebook-oauth2')
|
||||
SOCIAL_AUTH_SLUGIFY_USERNAMES = True
|
||||
SOCIAL_AUTH_CHANGE_SIGNAL_ONLY = False
|
||||
|
||||
LOGIN_URL = '/membership/login/'
|
||||
LOGIN_ERROR_URL = '/membership/login/error/'
|
||||
LOGIN_REDIRECT_URL = 'membership-details'
|
||||
SOCIAL_AUTH_NEW_USER_REDIRECT_URL = 'membership-edit'
|
||||
|
||||
|
||||
SOCIAL_AUTH_FACEBOOK_KEY = '115196761917023'
|
||||
SOCIAL_AUTH_FACEBOOK_SECRET = '6edf715f0506a1177b5479f8cae47566'
|
||||
SOCIAL_AUTH_FACEBOOK_SCOPE = ['user_about_me', 'email']
|
||||
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = '547248583417.apps.googleusercontent.com'
|
||||
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = '7jofzTy558SikOSV3cuTvY17'
|
||||
SOCIAL_AUTH_TWITTER_KEY = 'c8UY7a5XrRlPFyuoUNJw'
|
||||
SOCIAL_AUTH_TWITTER_SECRET = 'xLhz4zwAn3Lyl9MmJAiL85IurdmYzjDpoQF36du8tWg'
|
||||
|
||||
SESSION_COOKIE_DOMAIN = '.kasu.at' # Die ganze Domain Kasu
|
||||
SESSION_COOKIE_AGE = 4838400 # Session dauer: 4 Wochen
|
||||
|
||||
########################################
|
||||
# Tools for the development enviroment #
|
||||
########################################
|
||||
|
||||
if DEBUG:
|
||||
# Load the Rosetta Translation Tool if available
|
||||
try:
|
||||
import rosetta # @UnusedImport
|
||||
INSTALLED_APPS.append('rosetta')
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Load the Django Debug Toolbar if installed
|
||||
try:
|
||||
import debug_toolbar # @UnusedImport
|
||||
INSTALLED_APPS.append('debug_toolbar')
|
||||
MIDDLEWARE_CLASSES.append('debug_toolbar.middleware.DebugToolbarMiddleware') # @IgnorePep8
|
||||
except ImportError:
|
||||
pass
|
||||
29
src/templates/404.html
Normal file
29
src/templates/404.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}404 - Seite nicht gefunden{% endblock %}
|
||||
|
||||
{% block maincontent %}
|
||||
<h1 class="grid_12">404 - Nōten!</h1>
|
||||
<p class="grid_12">Unter der Adresse <em>{{request.path}}</em> wohnt zur Zeit kein Inhalt.<br />
|
||||
Unsere Empfehlung: Gehe zurück zum <a href="/">Start</a>.</p>
|
||||
|
||||
|
||||
<div class="grid_6">
|
||||
<h2>Ursachen</h2>
|
||||
<p>Tote Links sind ein bislang ungelöstes Problem des Hypertextes und im Kern darauf zurückzuführen, dass das Internet ein dezentrales Netzwerk ist. Daher kann die Integrität desselbigen niemals sichergestellt werden.</p>
|
||||
<p>Die Gründe für das Existieren von toten Links können sehr unterschiedlich sein und vor allem an sehr unterschiedlichen Stellen auftreten. In der Praxis häufig anzutreffende Gründe sind unter anderem:</p>
|
||||
<ul>
|
||||
<li>Die Datei, auf die die URL verweist, ist verschoben, umbenannt oder gelöscht worden.</li>
|
||||
<li>Der Link auf der Ursprungseite ist fehlerhaft angegeben, beispielsweise bei der Groß- oder Kleinschreibung im URL-Bestandteil (URLs sind case sensitive) nach der Angabe des Domainnamens.</li>
|
||||
<li>Die gesuchte Domain ist neu bei einem Registrar vergeben worden und die Website selber noch ohne jeden Inhalt.</li>
|
||||
<li>Der Webserver ist nicht (mehr) erreichbar, beispielsweise wegen Netzproblemen oder weil der Rechner abgeschaltet ist.</li>
|
||||
<li>Der Host- bzw. Domainname der URL existiert nicht (mehr) oder kann nicht im DNS aufgelöst werden.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="grid_6">
|
||||
<h2>Lösungsmöglichkeiten</h2>
|
||||
<p>Eine Möglichkeit, die allerdings eher die Symptome der Dead Links bekämpft, ist die <a href="http://de.wikipedia.org/wiki/Internet_Archive">Wayback Machine</a> des Internetarchivs. Durch chronologische Kopien von Webseiten kann Zugriff auf teilweise längst nicht mehr im Original vorhandene Webseiten gewährt werden.</p>
|
||||
<p>Ursachenbekämpfung betreiben hingegen Gremien wie das W3C mit der Kampagne Cool URIs don't change[1], die in der Öffentlichkeit das Bewusstsein für die Notwendigkeit persistenter URLs wecken. Zudem gibt es bereits Systeme, die das Ziel von unabänderlichen URLs verfolgen, beispielsweise das des Persistent Uniform Resource Locators.</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
130
src/templates/base.html
Normal file
130
src/templates/base.html
Normal file
@@ -0,0 +1,130 @@
|
||||
{% load i18n %}
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>Kasu - {% block title %}{{ current_top_page.menu_name|default:"traditionelle asiatische Spielkultur"}}{% endblock %}</title>
|
||||
<meta name="keywords" content="{% block keywords %}{% endblock %}" />
|
||||
<meta name="description" content="{% block description %}{% endblock %}" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="shortcut icon" href="{{STATIC_URL}}img/favicon.ico">
|
||||
<link rel="stylesheet" media="all" href="{{STATIC_URL}}css/common.css" type="text/css">
|
||||
<link rel="stylesheet" media="print" href="{{STATIC_URL}}css/print.css" type="text/css">
|
||||
<link rel="stylesheet" media="screen and (min-width: 701px)" href="{{STATIC_URL}}css/desktop.css" type="text/css">
|
||||
<link rel="stylesheet" media="screen and (max-width: 700px)" href="{{STATIC_URL}}css/mobile.css" type="text/css">
|
||||
<!--[if lt IE 9]>
|
||||
<link rel="stylesheet" media="screen" href="{{STATIC_URL}}css/desktop.css" type="text/css">
|
||||
<script type="text/javascript">
|
||||
document.createElement('header');
|
||||
document.createElement('nav');
|
||||
document.createElement('section');
|
||||
document.createElement('article');
|
||||
document.createElement('aside');
|
||||
document.createElement('footer');
|
||||
document.createElement('hgroup');
|
||||
</script>
|
||||
<![endif]-->
|
||||
<link rel="alternate" type="application/rss+xml" title="{% trans 'Current News' %}" href="{% url 'feed-latest-news' %}" />
|
||||
<link rel="alternate" type="application/rss+xml" title="{% trans 'Recent Comments' %}" href="{% url 'feed-latest-comments' %}" />
|
||||
{% block opengraph %}
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="Kasu - Verein Japanische Spielekultur" />
|
||||
<meta property="og:url" content="http://www.kasu.at{{ request.path_info }}" />
|
||||
<meta property="og:image" content="http://www.kasu.at/static/img/logo.png" />
|
||||
{% endblock %}
|
||||
{% block extra_head %}{% endblock %}
|
||||
</head>
|
||||
<body {% block itemscope %}{% endblock %}>
|
||||
<div id="header_bg"></div>
|
||||
<header id="header">
|
||||
<h1 id="sitelogo"><a href="/index.html">Kasu - traditionelle asiatische Spielkultur</a></h1>
|
||||
<nav id="mainnav">
|
||||
<input type="checkbox" id="toggle" />
|
||||
<label for="toggle" class="toggle" onclick>{% trans "Menu" %}</label>
|
||||
<ul class="main_menu">
|
||||
{% for item in top_menu_items %}
|
||||
<li><a href="{{item.get_absolute_url}}" title="{{ item.title }}" {%if item.active %}class="active"{% endif %}>{{item.menu_name}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
<div id="maincontent">
|
||||
{% block navigation %}
|
||||
{% if current_top_page.subpages.count %}
|
||||
<ul id="navigation">
|
||||
<li><a href="{{current_top_page.get_absolute_url}}" {% ifequal current_page current_top_page %}class="active"{% endifequal %}>{{current_top_page.menu_name}}</a></li>
|
||||
{% for subpage in current_top_page.subpages.all %}
|
||||
<li><a href="{{subpage.get_absolute_url}}" {% ifequal subpage current_page %}class="active"{% endifequal %}>{{subpage.menu_name}}</a></li>
|
||||
{% endfor %}
|
||||
{% block additional_nav_elements %}{% endblock %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block messages %}
|
||||
{% if messages %}
|
||||
<ul id="messages" class="grid_12">
|
||||
{% for message in messages %}
|
||||
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block maincontent %}
|
||||
<div id="content">{% block content %}{% endblock %}</div>
|
||||
<aside id="sidebar">{% block sidebar %}{% endblock %}</aside>
|
||||
{% endblock %}
|
||||
{% block paginator %}{% if page_obj.has_other_pages %}{% include 'paginator.html' %}{% endif %}{% endblock %}
|
||||
{% block comments %}{% endblock %}
|
||||
<p id="bottom_buttonbar" class="buttonbar grid_12">
|
||||
{% block buttonbar %}
|
||||
{% if current_page and perms.content.change_page %}<a href="{% url 'edit-page' current_page.path %}" class="button"><img src="{{ STATIC_URL }}icons/page_edit.png" alt="" /> {% trans "Edit Page" %}</a>{% endif %}
|
||||
{% if current_page and perms.content.add_page %}<a href="{% url 'add-page' current_page.path %}" class="button"><img src="{{ STATIC_URL }}icons/page_add.png" alt="" /> {% trans "Add Subpage" %}</a>{% endif %}
|
||||
{% block additional_buttonbar %}{% endblock %}
|
||||
{% endblock %}
|
||||
</p>
|
||||
</div>
|
||||
<br class="clear" />
|
||||
<div id="footer_bg"> </div>
|
||||
<footer id="footer">
|
||||
<p><strong>Herausgeber:</strong> Verein Kasu - traditionelle asiatische Spielkultur (<a href="/verein/impressum.html">{% trans "imprint" %}</a> – <a href='mailto:ve%72e%69n@%6B%61su.a%74'>{% trans "contact" %}</a>)</p>
|
||||
<form action="/i18n/setlang/" method="post" style="text-align:right;">
|
||||
{% csrf_token %}
|
||||
<label for="language">{% trans "Language" %}:</label>
|
||||
<select name="language" id="language">
|
||||
{% get_language_info_list for LANGUAGES as languages %}
|
||||
{% for language in languages %}
|
||||
<option value="{{language.code}}" {% ifequal language.code LANGUAGE_CODE %}
|
||||
selected="selected"{% endifequal %}>{{ language.name_local }} ({{ language.code }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit">{% trans "Go" %}</button>
|
||||
</form>
|
||||
</footer>
|
||||
<nav id="usernav">
|
||||
{% if user.is_authenticated %}
|
||||
{% trans "Logged in as" %}:
|
||||
<a rel="nofollow" href="{% url 'membership-details' user.username %}">{{user.username}}</a> -
|
||||
{% if user.is_staff %}<a href="/admin">{% trans "Admin" %}</a>{% endif %}
|
||||
<a rel="nofollow" href="{% url 'logout' %}?next={{ request.path_info }}">{% trans "Logout" %}</a>
|
||||
{% else %}
|
||||
{% trans "no user logged in" %} -
|
||||
<a rel="nofollow" href="{% url 'membership-register' %}">{% trans "register" %}</a>
|
||||
<a rel="nofollow" href="{% url 'login' %}?next={{ request.path_info }}">{% trans "login" %}</a>
|
||||
<a rel="nofollow" href="{% url 'social:begin' 'facebook' %}"><img src="{{STATIC_URL}}img/facebook.png" alt="Facebook Login" title="Login with Facebook" width="26" height="26"/></a>
|
||||
<a rel="nofollow" href="{% url 'social:begin' 'twitter' %}"><img src="{{STATIC_URL}}img/twitter.png" alt="Twitter Login" title="Login with Twitter" width="26" height="26"/></a>
|
||||
<a rel="nofollow" href="{% url 'social:begin' 'google-oauth2' %}"><img src="{{STATIC_URL}}img/google.png" alt="Google Login" title="Login with Google" width="26" height="26"/></a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
<script type="text/javascript">
|
||||
{% block javascript %}{% endblock %}
|
||||
var pkBaseURL = (("https:" == document.location.protocol) ? "https://admin.animanga.at/tools/piwik/" : "http://admin.animanga.at/tools/piwik/");
|
||||
document.write(unescape("%3Cscript src='" + pkBaseURL + "piwik.js' type='text/javascript'%3E%3C/script%3E"));
|
||||
try {
|
||||
var piwikTracker = Piwik.getTracker(pkBaseURL + "piwik.php", 5);
|
||||
piwikTracker.trackPageView();
|
||||
piwikTracker.enableLinkTracking();
|
||||
} catch( err ) {
|
||||
|
||||
}
|
||||
</script><noscript><p><img src="http://admin.animanga.at/tools/piwik/piwik.php?idsite=5" style="border:0" alt="" /></p></noscript>
|
||||
</body>
|
||||
</html>
|
||||
44
src/templates/comments/form.html
Normal file
44
src/templates/comments/form.html
Normal file
@@ -0,0 +1,44 @@
|
||||
{% load comments i18n %}
|
||||
<form action="{% comment_form_target %}" method="post" id='comment_form'>
|
||||
{% csrf_token %}
|
||||
<fieldset class="grid_12">
|
||||
<legend>{% trans "New Comment" %}</legend>
|
||||
{% if user.is_authenticated %}
|
||||
<p style="display:none">
|
||||
{{form.honeypot}} {{form.honeypot.label}}
|
||||
{% for field in form.hidden_fields %}{{ field }}{% endfor %}
|
||||
{% if next %}
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
{% else %}
|
||||
<input type="hidden" name="next" value="{{ request.path_info }}" />
|
||||
</p>
|
||||
{% endif %}
|
||||
<label class="field_name" for="id_comment">
|
||||
{{user}}<br/>
|
||||
<time class="submit_date" datetime='{% now "c" %}'>{% now "d. M. Y. H:i" %}</time>
|
||||
{% if user.get_profile.thumbnail %}<img src="{{user.get_profile.thumbnail.url}}" alt=""/>{% else %}<img src="{{STATIC_URL}}img/unknown_thumbnail.png" alt=""/>{% endif %}
|
||||
</label>
|
||||
{{form.comment}}
|
||||
<p class="buttonbar">
|
||||
<button type="submit" name="preview"><img src="{{ STATIC_URL }}icons/comment_edit.png" alt="{% trans "Preview" %}" /> {% trans "Preview" %}</button>
|
||||
<button type="submit" name="submit"><img src="{{ STATIC_URL }}icons/comment_add.png" alt="{% trans "Post" %}" /> {% trans "Post" %}</button>
|
||||
</p>
|
||||
{% else %}
|
||||
<label class="field_name" for="id_comment">
|
||||
{% trans "not logged in" %}<br/>
|
||||
<time class="submit_date" datetime='{% now "c" %}'>{% now "d. M. Y. H:i" %}</time>
|
||||
<img src="{{STATIC_URL}}img/unknown_thumbnail.png" alt=""/>
|
||||
</label>
|
||||
<span>
|
||||
{% url "membership-register" as register %}
|
||||
{% url "login" as login %}
|
||||
{% blocktrans with request.path_info as next %}<a href="{{ register }}">Register</a> now, or <a href="{{ login }}?next={{ next }}">Login</a> to leave a comment here.{% endblocktrans %}
|
||||
</span>
|
||||
<p class="buttonbar">
|
||||
<a href="{{ register }}" class="button"><img src="{{STATIC_URL}}icons/user_add.png" />{% trans 'register' %}</a>
|
||||
<a href="{{ login }}" class="button"><img src="{{STATIC_URL}}icons/lock_break.png" />{% trans 'login' %}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
<p> </p>
|
||||
</form>
|
||||
15
src/templates/comments/list.html
Normal file
15
src/templates/comments/list.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% load i18n django_markdown %}
|
||||
<table id="comments" class="layout">
|
||||
{% for comment in comment_list %}
|
||||
<tr id="c{{ comment.id }}">
|
||||
<td class="avatar">
|
||||
{% if comment.user.get_profile.thumbnail %}<img src="{{comment.user.get_profile.thumbnail.url}}" alt=""/>{% else %}<img src="{{STATIC_URL}}img/unknown_thumbnail.png" alt=""/>{% endif %}
|
||||
</td>
|
||||
<td class="userinfo">
|
||||
<a href="{{ comment.user.get_profile.get_absolute_url }}" class="user">{{comment.user}}</a>
|
||||
<div class="submit_date"><time time="{{comment.submit_date|time:'c'}}">{{comment.submit_date|timesince}}</time></div>
|
||||
</td>
|
||||
<td class="content">{{comment.comment|markdown_safe}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
8
src/templates/comments/posted.html
Normal file
8
src/templates/comments/posted.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Thank you for your comment" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{% trans "Thank you for your comment" %}!</h1>
|
||||
{% endblock %}
|
||||
26
src/templates/comments/preview.html
Normal file
26
src/templates/comments/preview.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n django_markdown %}
|
||||
|
||||
{% block title %}{% trans "Preview your comment" %}{% endblock %}
|
||||
|
||||
{% block maincontent %}
|
||||
{% if form.errors %}
|
||||
<h2>{% blocktrans count form.errors|length as counter %}Please correct the error below{% plural %}Please correct the errors below{% endblocktrans %}</h2>
|
||||
{% else %}
|
||||
<h2>{% trans "Preview your comment" %}</h2>
|
||||
<table id="comments" class="layout">
|
||||
<tr id="c{{ comment.id }}">
|
||||
<td class="avatar">
|
||||
{% if user.get_profile.thumbnail %}<img src="{{user.get_profile.thumbnail.url}}" alt=""/>{% else %}<img src="{{STATIC_URL}}img/unknown_thumbnail.png" alt=""/>{% endif %}
|
||||
</td>
|
||||
<td class="userinfo">
|
||||
<div class="user">{{user}}</div>
|
||||
<time class="submit_date" datetime='{% now "c" %}'>{% now "d. m. Y. h:i" %}</time>
|
||||
</td>
|
||||
<td class="content">{{comment|markdown_safe}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endif %}
|
||||
<br class="clear" />
|
||||
{% include "comments/form.html" %}
|
||||
{% endblock %}
|
||||
94
src/templates/content/article_archive.html
Normal file
94
src/templates/content/article_archive.html
Normal file
@@ -0,0 +1,94 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n comments %}
|
||||
{% block title %}
|
||||
{% trans 'Article Archive' %}
|
||||
{% if active_category %} - {{active_category.name}}{% endif %}
|
||||
{% if month %}{{ month|date:'F Y' }}</h2>{% elif year %}{{year}}{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block meta_title %}
|
||||
{% trans 'Article Archive' %}
|
||||
{% if active_category %} - {{active_category.name}}{% endif %}
|
||||
{% if month %}{{ month|date:'F Y' }}</h2>{% elif year %}{{year}}{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<style type="text/css" media="screen and (min-width: 701px)">#teaser{background-image:url("{{STATIC_URL}}img/news_teaser.jpg")}</style>
|
||||
<!--[if lt IE 9]><style type="text/css" media="screen">#teaser{background-image:url("{{STATIC_URL}}img/news_teaser.jpg")}</style><![endif]-->
|
||||
{% endblock %}
|
||||
|
||||
{% block navigation %}
|
||||
<ul id="navigation">
|
||||
<li><a href="{{current_top_page.get_absolute_url}}" {% if not active_category %}class="active"{% endif %}>{% trans 'All Categories' %}</a></li>
|
||||
{% for category in categories %}
|
||||
<li><a href="{% url 'article-archive' category=category.slug %}"
|
||||
{% ifequal category.slug active_category.slug %}class="active"{% endifequal %}>{{ category.name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<!--[if lt IE 9]><style type="text/css" media="screen">#teaser{background-image:url("{{STATIC_URL}}img/news_teaser.jpg")}</style><![endif]-->
|
||||
{% endblock %}
|
||||
|
||||
{% block maincontent %}
|
||||
<section id="teaser" class="grid_8">
|
||||
<div id="teaser_text">
|
||||
<h2>{% trans 'Article Archive' %}
|
||||
{% if active_category %} - {{active_category.name}}{% endif %}
|
||||
{% if month %}{{ month|date:'F Y' }}</h2>{% elif year %}{{year}}{% endif %}
|
||||
</h2>
|
||||
{{content|safe}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="grid_4 red_box">
|
||||
{% block date_list %}
|
||||
<h2>{% trans "Archive" %} {{year}}</h2>
|
||||
<ul class="list" style="margin: 20px;">
|
||||
{% if active_category %}
|
||||
{% for date in date_list %}
|
||||
<li class="date"><a href="{% url "article-archive" category=active_category.slug year=date|date:'Y' %}">{{active_category.name}}: {{ date|date:'Y' }}</a></li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for date in date_list %}
|
||||
<li class="date"><a href="{% url "article-archive" year=date|date:'Y' %}">{{ date|date:'Y' }}</a></li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
</section>
|
||||
|
||||
<section class="grid_8">
|
||||
{% for article in article_list %}
|
||||
{% get_comment_count for article as comment_count %}
|
||||
<article class="article">
|
||||
<h3><a href="{{article.get_absolute_url}}">{{article.headline}}</a></h3>
|
||||
<ul class="info">
|
||||
<li><img src="{{STATIC_URL}}icons/calendar.png" alt="{% trans 'created on' %}:" title="{% trans 'created on' %}"/> <time datetime="{{article.date_created|date:"c"}}">{{ article.date_created|date }}</time></li>
|
||||
<li><img src="{{STATIC_URL}}icons/user_red.png" alt="{% trans 'by' %}:" title="{% trans 'by' %}"/> <span class="author">{{ article.author }}</span></li>
|
||||
<li><img src="{{STATIC_URL}}icons/comments.png" alt="{% trans 'comments' %}:" title="{% trans 'comments' %}"/> <a href="{{article.get_absolute_url}}#comments" >{{comment_count}} {% trans "comments" %}</a></li>
|
||||
</ul>
|
||||
<a href="{{article.get_absolute_url}}"><img src="{{article.posting_image.url}}" alt="{{article.category}}:" class="posting_image"/></a>
|
||||
{{article.content|truncatewords_html:50}}
|
||||
<p class="more_link"><a href="{{article.get_absolute_url}}" class="button">{% trans "Read More"%} <img src="{{STATIC_URL}}icons/page_go.png" alt="»" /></a></p>
|
||||
</article>
|
||||
{% empty %}
|
||||
<p>{% trans "We're sorry. Your search yielded no results." %}</p>
|
||||
{% endfor %}
|
||||
</section>
|
||||
<aside class="grid_4">
|
||||
{% for feed in feeds %}
|
||||
<h2><a href="{{feed.public_url}}">{{feed.title}}</a></h2>
|
||||
<ul>
|
||||
{% for item in feed.feed_items.all %}
|
||||
<li><a href="{{item.link}}">{{item.title}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
</aside>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block additional_buttonbar %}
|
||||
{% if perms.content.add_article %}
|
||||
<a href="{% url "add-article" %}" class="button"><img src="{{ STATIC_URL }}icons/note_add.png" alt="" /> {% trans "Add Article" %}</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
9
src/templates/content/article_archive_month.html
Normal file
9
src/templates/content/article_archive_month.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{% extends "content/article_archive.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block date_list %}
|
||||
<h2>{% trans "Archive" %} {{month|date:'E'}}</h2>
|
||||
<div class="buttonbar">
|
||||
<a href="{% url "article-archive" year=month|date:'Y' %}" class="button"><img src="{{ STATIC_URL }}icons/arrow_undo.png" alt="{% trans 'back' %}" /> {% trans 'back' %}</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
20
src/templates/content/article_archive_year.html
Normal file
20
src/templates/content/article_archive_year.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{% extends "content/article_archive.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block meta_title %}{{ article.headline }}{% endblock %}
|
||||
|
||||
{% block date_list %}
|
||||
<h2>{% trans "Archive" %} {{year}}</h2>
|
||||
|
||||
<ul class="list" style="margin: 20px;">
|
||||
{% if active_category %}
|
||||
{% for date in date_list %}
|
||||
<li class="date"><a href="{% url "article-archive" category=active_category.slug year=year|date:'Y' month=date|date:'m' %}">{{active_category.name}}: {{ date|date:'F' }}</a></li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for date in date_list %}
|
||||
<li class="date"><a href="{% url "article-archive" year=year|date:'Y' month=date|date:'m' %}">{{ date|date:'F' }}</a></li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
54
src/templates/content/article_detail.html
Normal file
54
src/templates/content/article_detail.html
Normal file
@@ -0,0 +1,54 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n comments %}
|
||||
|
||||
{% block title %}{{ article.headline }}{% endblock %}
|
||||
|
||||
{% block description %}{{article.content|striptags|truncatewords:16}}{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<link rel="image_src" type="image/jpeg" href="{{article.posting_image.url}}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block opengraph %}
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:title" content="{{ article.headline }}" />
|
||||
<meta property="og:url" content="http://www.kasu.at{{ article.get_absolute_url }}" />
|
||||
<meta property="og:image" content="http://www.kasu.at{{article.posting_image.url}}" />
|
||||
<meta property="og:description" content="{{article.content|striptags|truncatewords:25}}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block itemscope %}itemscope itemtype="http://schema.org/Article"{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2 itemprop="name">{{article.headline}}</h2>
|
||||
<div itemprop="articleBody">{{ article.content }}</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
<img alt="{{article.category.name}}" src="{{article.posting_image.url}}" class="posting_image" itemprop="image"/>
|
||||
<p> </p>
|
||||
|
||||
<ul class="info">
|
||||
<li class="user"><strong>{% trans 'Author' %}:</strong> <a href="{% url "membership-details" article.author %}" itemprop="author">{{article.author}}</a></li>
|
||||
<li class="date"><strong>{% trans 'Created on' %}: </strong><time date="{{article.date_created|date:"c"}}">{{ article.date_created|date }}</time></li>
|
||||
<li class="category"><strong>{% trans "Category"%}: </strong><a href="{{ article.category.get_absolute_url }}" itemprop="articleSection">{{article.category.name}}</a></li>
|
||||
</ul>
|
||||
|
||||
<p class="center">
|
||||
<a href="https://plus.google.com/share?url=http%3A%2F%2Fwww.kasu.at{{article.get_absolute_url|urlencode}}" onclick="javascript:window.open(this.href,
|
||||
'', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=600,width=600');return false;"><img src="{{STATIC_URL}}img/google_plus.png" alt="Google+" title="{% trans 'Share on Google+'%}" width="39" height="39"/></a>
|
||||
<a href="https://twitter.com/share?url=http%3A%2F%2Fwww.kasu.at{{article.get_absolute_url|urlencode}}" target='_blank'><img src="{{STATIC_URL}}img/twitter.png" alt="Twitter" title="{% trans 'Share on Twitter' %}" width="39" height="39"/></a>
|
||||
<a href="http://facebook.com/sharer.php?u=http%3A%2F%2Fwww.kasu.at{{article.get_absolute_url|urlencode}}?t={{article.headline|urlencode}}" target="_blank"><img src="{{STATIC_URL}}img/facebook.png" alt="Facebook" title="{% trans 'Share on Facebook'%}" width="39" height="39"/></a>
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block comments%}
|
||||
{% render_comment_list for article %}
|
||||
{% render_comment_form for article %}
|
||||
{% endblock %}
|
||||
|
||||
{% block buttonbar %}
|
||||
{% if perms.content.change_article %}
|
||||
<a href="{% url "edit-article" article.id %}" class="button"><img src="{{STATIC_URL}}icons/note_edit.png" alt="" />{% trans "Edit Article" %}</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
52
src/templates/content/article_form.html
Normal file
52
src/templates/content/article_form.html
Normal file
@@ -0,0 +1,52 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n fieldset_extras %}
|
||||
|
||||
{% block title %}
|
||||
{% if object.pk %}{% trans "Edit Article" %}{% else %}{% trans "Create Article" %}{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script type="text/javascript" src="{{ STATIC_URL }}js/jquery-1.6.2.min.js"></script>
|
||||
<script type="text/javascript" src="{{ STATIC_URL }}js/language_tabs.js" ></script>
|
||||
<script type="text/javascript" src="{{ STATIC_URL }}js/ckeditor/ckeditor.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block maincontent %}
|
||||
{% get_fieldset "category, image" from form as fieldset_common %}
|
||||
{% get_fieldset "headline_de, content_de" from form as fieldset_de %}
|
||||
{% get_fieldset "headline_en, content_en" from form as fieldset_en %}
|
||||
|
||||
<form action="" method="post" enctype="multipart/formdata" class="grid_12">
|
||||
<fieldset>
|
||||
<legend>{% if object.pk %}{% trans "Edit Article" %}{% else %}{% trans "Create Article" %}{% endif %}</legend>
|
||||
{% csrf_token %}
|
||||
{% with fieldset_common as form %}{% include "form.html" %}{% endwith %}
|
||||
</fieldset>
|
||||
<ul class="tabs">
|
||||
<li><a href="#de">{% trans "German" %}</a></li>
|
||||
<li><a href="#en">{% trans "English" %}</a></li>
|
||||
</ul>
|
||||
<div class="tab_container">
|
||||
<fieldset id="de" class="tab_content">
|
||||
<legend>Deutsch</legend>
|
||||
{% with fieldset_de as form %}{% include "form.html" %}{% endwith %}
|
||||
</fieldset>
|
||||
<fieldset id="en" class="tab_content">
|
||||
<legend>English</legend>
|
||||
{% with fieldset_en as form %}{% include "form.html" %}{% endwith %}
|
||||
</fieldset>
|
||||
</div>
|
||||
<p class="buttonbar">
|
||||
<button type="reset"><img src="{{STATIC_URL}}icons/arrow_undo.png" alt="{% trans 'reset' %}" /> {% trans 'reset' %}</button>
|
||||
<button type="submit"><img src="{{STATIC_URL}}icons/disk.png" alt="{% trans 'save' %}" /> {% trans 'save' %}</button>
|
||||
</p>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
CKEDITOR.replace( 'id_content_de' );
|
||||
CKEDITOR.replace( 'id_content_en' );
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block buttonbar %}{% endblock %}
|
||||
27
src/templates/content/clear_page.html
Normal file
27
src/templates/content/clear_page.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load comments %}
|
||||
|
||||
{% block meta_title %}{{ page.title }}{% endblock %}
|
||||
|
||||
{% block title %}{{page.title}}{% endblock %}
|
||||
|
||||
{% block opengraph %}
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="{{page.title}}" />
|
||||
<meta property="og:url" content="http://www.kasu.at{{ page.get_absolute_url }}" />
|
||||
<meta property="og:image" content="http://www.kasu.at/static/img/logo.png" />
|
||||
<meta property="og:description" content="{{ page.content|striptags|truncatewords:25 }}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block maincontent %}
|
||||
<h2 class="grid_12">{{ page.title }}</h2>
|
||||
{{ page.content }}
|
||||
{% endblock %}
|
||||
|
||||
{% block comments %}
|
||||
{% if page.enable_comments %}
|
||||
{% render_comment_list for page %}
|
||||
{% render_comment_form for page %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
36
src/templates/content/page.html
Normal file
36
src/templates/content/page.html
Normal file
@@ -0,0 +1,36 @@
|
||||
{% extends "base.html" %}
|
||||
{% load comments i18n %}
|
||||
|
||||
{% block meta_title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block title %}{{page.title}}{% endblock %}
|
||||
|
||||
{% block opengraph %}
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="{{page.title}}" />
|
||||
<meta property="og:url" content="http://www.kasu.at{{ page.get_absolute_url }}" />
|
||||
<meta property="og:image" content="http://www.kasu.at/static/img/logo.png" />
|
||||
<meta property="og:description" content="{{ page.content|striptags|truncatewords:25 }}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>{{ page.title }}</h2>
|
||||
{{ page.content }}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% if current_top_page.subpages %}
|
||||
<h2>{% trans "Subpages" %}</h2>
|
||||
{% for subpage in current_top_page.subpages.all %}
|
||||
<h3><a href="{{ subpage.get_absolute_url }}">{{subpage.menu_name}}</a></h3>
|
||||
<ul class="list">
|
||||
{% for subpage in subpage.subpages.all %}<li class="{{subpage.css_class}}"><a href="{{ subpage.get_absolute_url }}">{{ subpage.menu_name }}</a></li>{% endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block comments %}{% if page.enable_comments %}
|
||||
{% render_comment_list for page %}
|
||||
{% render_comment_form for page %}
|
||||
{% endif %}{% endblock %}
|
||||
58
src/templates/content/page_form.html
Normal file
58
src/templates/content/page_form.html
Normal file
@@ -0,0 +1,58 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n fieldset_extras %}
|
||||
|
||||
{% block title %}{% if object.pk %}{% trans "Edit Page" %}: {{ page.title }}{% else %}{% trans "Add Page" %}{% endif %}{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script type="text/javascript" src="{{ STATIC_URL }}js/jquery-1.6.2.min.js"></script>
|
||||
<script type="text/javascript" src="{{ STATIC_URL }}js/language_tabs.js" ></script>
|
||||
<script type="text/javascript" src="{{ STATIC_URL }}js/ckeditor/ckeditor.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block maincontent %}
|
||||
|
||||
{% get_fieldset "parent, slug, content_type, status" from form as fieldset_common %}
|
||||
{% get_fieldset "menu_name_de, title_de, pdf_de, content_de" from form as fieldset_de %}
|
||||
{% get_fieldset "menu_name_en, title_en, pdf_en, content_en" from form as fieldset_en %}
|
||||
{% get_fieldset "template, enable_comments" from form as fieldset_html %}
|
||||
|
||||
<form method="post" enctype="multipart/form-data" class="grid_12">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
<legend>{% if object.pk %}{% trans "Edit Page" %}{% else %}{% trans "Add Page" %}{% endif %}</legend>
|
||||
{% with fieldset_common as form %}{% include "form.html" %}{% endwith %}
|
||||
</fieldset>
|
||||
|
||||
<ul class="tabs">
|
||||
<li><a href="#de">{% trans "German" %}</a></li>
|
||||
<li><a href="#en">{% trans "English" %}</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="tab_container">
|
||||
<fieldset id="de" class="tab_content">
|
||||
<legend>{% trans "German" %}</legend>
|
||||
{% with fieldset_de as form %}{% include "form.html" %}{% endwith %}
|
||||
</fieldset>
|
||||
<fieldset id="en" class="tab_content">
|
||||
<legend>{% trans "English" %}</legend>
|
||||
{% with fieldset_en as form %}{% include "form.html" %}{% endwith %}
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<legend>{% trans "HTML Specific" %}</legend>
|
||||
{% with fieldset_html as form %}{% include "form.html" %}{% endwith %}
|
||||
</fieldset>
|
||||
|
||||
<p class="buttonbar">
|
||||
<button type="reset"><img src="{{STATIC_URL}}icons/arrow_undo.png" alt="{% trans 'reset' %}" /> {% trans 'reset' %}</button>
|
||||
<button type="submit"><img src="{{STATIC_URL}}icons/page_save.png" alt="{% trans 'save' %}" /> {% trans 'save' %}</button>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
CKEDITOR.replace( 'id_content_de' );
|
||||
CKEDITOR.replace( 'id_content_en' );
|
||||
{% endblock %}
|
||||
66
src/templates/content/page_pdf.html
Normal file
66
src/templates/content/page_pdf.html
Normal file
@@ -0,0 +1,66 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
|
||||
<html>
|
||||
<head>
|
||||
<title>{% block title %}{{page.title}}{% endblock %}</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'Amerika Sans';
|
||||
src: url('{{ STATIC_ROOT }}/fonts/amerika_sans.ttf');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Philosopher';
|
||||
src: url('{{ STATIC_ROOT }}/fonts/philosopher-regular.ttf');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
h1,h2,h3,h4,h5,h6 {
|
||||
font-family: 'Amerika Sans', Helvetica;
|
||||
font-variant: small-caps;
|
||||
font-weight: bold;
|
||||
margin: 0.5em 0 0.2em 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Philosopher;
|
||||
font-size: 12pt;
|
||||
}
|
||||
|
||||
#page_header {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@page {
|
||||
margin-right: 0mm;
|
||||
margin-bottom: 0mm;
|
||||
margin-top: 35mm;
|
||||
margin-left: 2cm;
|
||||
@frame header {
|
||||
-pdf-frame-content : page_header;
|
||||
top: 0mm;
|
||||
margin: 5mm;
|
||||
height: 4cm;
|
||||
}
|
||||
@frame footer {
|
||||
-pdf-frame-content: page_footer;
|
||||
bottom: 0cm;
|
||||
height: 2cm;
|
||||
margin: 5mm;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{ page.title }}</h1>
|
||||
{{ page.content }}
|
||||
<div id="page_header">
|
||||
<img src="{{STATIC_ROOT}}/img/logo.png" alt="Kasu">
|
||||
</div>
|
||||
<div id="page_footer">
|
||||
{{ page.title }} Seite:
|
||||
<pdf:pagenumber>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
51
src/templates/events/event_archive.html
Normal file
51
src/templates/events/event_archive.html
Normal file
@@ -0,0 +1,51 @@
|
||||
{% extends "events/page.html" %}
|
||||
{% load i18n comments%}
|
||||
|
||||
{% block title %}
|
||||
{% trans 'Event Archive' %} {% if month %}{{ month|date:'F Y' }} {% else %}{% if year %}{{year}}{% endif %}{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>{% trans 'Event Archive' %} {% if month %}{{ month|date:'F Y' }} {% else %}{% if year %}{{year}}{% endif %}{% endif %}</h2>
|
||||
|
||||
{% for event in event_list %}
|
||||
{% ifchanged %}<h3 class="clearfix">{{ event.start|date:'F Y' }}</h3>{% endifchanged %}
|
||||
<div class="clearfix">
|
||||
{% get_comment_count for event as comment_count %}
|
||||
<a href="{{ event.get_absolute_url }}"><img src="{{ event.get_thumbnail.url }}" alt=" {% trans 'Event Image' %}" class="thumbnail"/></a>
|
||||
<h4><a href="{{ event.get_absolute_url }}">{{ event.name }}</a></h4>
|
||||
{{event.description}}
|
||||
<img src="{{ STATIC_URL }}/icons/date.png" alt="{% trans 'Start' %}:" title="{% trans 'Start' %}">
|
||||
{{ event.start|date }}
|
||||
{% if event.end %}
|
||||
{% trans "from" %} {{ event.start|time:'H:i' }} {% trans "to" %} {{ event.end|time:'H:i' }}
|
||||
{% else %}
|
||||
{{ event.start|time:'H:i' }}
|
||||
{% endif %}
|
||||
<ul class="info">
|
||||
<li class="location">{{ event.location }}</li>
|
||||
<li class="comment"><a href="{{event.get_absolute_url}}#comments">{{ comment_count }} {% trans 'Comments' %}</a></li>
|
||||
<li class="photo"><a href="{% url 'event-photo-list' event.pk %}">{{ event.photo_count }} {% trans 'Photos' %}</a></li>
|
||||
<li class="hanchan"><a href="{% url 'event-hanchan-list' event.pk %}">{{ event.hanchan_set.count }} {% trans 'Hanchans' %}</a></li>
|
||||
{% if perms.events.change_event %}<li><a href="{{ event.get_edit_url }}" class="button"><img src="{{ STATIC_URL }}icons/page_edit.png" alt="{%trans "Edit" %}"></a></li>{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
<h2>Archive</h2>
|
||||
<ul>
|
||||
{% if year %}
|
||||
{% for date in date_list %}
|
||||
<li><a href="{% url 'event-archive' year|date:'Y' date|date:'m' %}">{{ date|date:'F' }}</a></li>
|
||||
{% endfor %}
|
||||
{% elif month %}
|
||||
<li><a href="{% url 'event-archive' month|date:'Y' %}">{{ month|date:'Y' }}</a></li>
|
||||
{% else %}
|
||||
{% for date in date_list %}
|
||||
<li><a href="{% url 'event-archive' date|date:'Y' %}">{{ date|date:'Y' }}</a></li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
111
src/templates/events/event_detail.html
Normal file
111
src/templates/events/event_detail.html
Normal file
@@ -0,0 +1,111 @@
|
||||
{% extends "events/page.html" %}
|
||||
{% load i18n django_markdown comments %}
|
||||
|
||||
{% block title %}{{ event.name }}{% endblock %}
|
||||
|
||||
{% block opengraph %}
|
||||
<meta property="og:type" content="activity" />
|
||||
<meta property="og:title" content="{{event.name}}" />
|
||||
<meta property="og:url" content="http://www.kasu.at{{event.get_absolute_url}}" />
|
||||
<meta property="og:image" content="http://www.kasu.at{{ event.get_thumbnail.url }}" />
|
||||
{% if event.description %}<meta property="og:description" content="{{event.description}}" />{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script type="text/javascript" src="https://maps.google.com/maps/api/js?sensor=false"></script>
|
||||
<style type="text/css" media="screen and (min-width: 701px)">#teaser{background-image:url('{{ event.get_callout.url }}')}</style>
|
||||
<!--[if lt IE 9]><style type="text/css" media="screen">#teaser{background-image:url("{{event.get_callout.url}}")}</style><![endif]-->
|
||||
{% endblock %}
|
||||
|
||||
{% block maincontent %}
|
||||
<div class="grid_8" id="teaser">
|
||||
{% if event.description %}
|
||||
<div id="teaser_text">
|
||||
<h2>{{event.name}}</h2>
|
||||
{{event.description|markdown}}
|
||||
</div>
|
||||
{% else %}
|
||||
<div id="teaser_text">
|
||||
<h2>{{event.name}}</h2>
|
||||
{{event.location.description|markdown}}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="grid_4 red_box" id="map_canvas" style="width:300px; height:300px;"></div>
|
||||
|
||||
<div class="grid_4">
|
||||
<h3>{% trans "Date" %}</h3>
|
||||
<p></p>
|
||||
<ul>
|
||||
<li><strong>{% trans "Start" %}:</strong> {{ event.start }}</li>
|
||||
{% if event.end %}<li><strong>{% trans "End" %}:</strong> {{ event.end }}</li>{% endif %}
|
||||
{% if event.url %}<li><strong>{% trans "Homepage" %}:</strong> <a href="{{ event.url }}">{{ event.url }}</a></li>{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="grid_4">
|
||||
<h3>{% trans 'Location' %}</h3>
|
||||
<strong>{{ event.location.name }}</strong>
|
||||
<address>
|
||||
{{event.location.street_address}}<br />
|
||||
{{event.location.postal_code}} {{event.location.locality}} <br />
|
||||
{{event.location.get_country_display}}
|
||||
</address>
|
||||
{% if event.location.url %}
|
||||
<p><strong>{% trans "Homepage" %}:</strong> <a href="{{ event.location.url }}">{{ event.location.url }}</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="grid_4 center">
|
||||
<ul class="info">
|
||||
<li><img src="{{ STATIC_URL }}icons/table.png" alt="{% trans 'Hanchans' %}" /><a href="{% url 'event-hanchan-list' event.pk %}" >{{ event.hanchan_set.count }} {% trans "Hanchans" %}</a></li>
|
||||
<li><img src="{{ STATIC_URL }}icons/camera.png" alt="{% trans 'Photos' %}" /><a href="{% url 'event-photo-list' event.pk %}">{{ event.photo_count }} {% trans 'Photos' %}</a></li>
|
||||
</ul>
|
||||
<a href="https://plus.google.com/share?url=http%3A%2F%2Fwww.kasu.at{{event.get_absolute_url|urlencode}}" onclick="javascript:window.open(this.href,
|
||||
'', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=600,width=600');return false;"><img src="{{STATIC_URL}}img/google_plus.png" alt="Google+" title="{% trans 'Share on Google+'%}" width="39" height="39"/></a>
|
||||
<a href="https://twitter.com/share?url=http%3A%2F%2Fwww.kasu.at{{event.get_absolute_url|urlencode}}" target='_blank'><img src="{{STATIC_URL}}img/twitter.png" alt="Twitter" title=" {% trans 'Share on Twitter' %}" width="39" height="39"/></a>
|
||||
<a href="http://facebook.com/sharer.php?u=http%3A%2F%2Fwww.kasu.at{{event.get_absolute_url|urlencode}}" target="_blank"><img src="{{STATIC_URL}}img/facebook.png" alt="Facebook" title="{% trans 'Share on Facebook'%}" width="39" height="39"/></a>
|
||||
<a href="http://maps.google.com/maps?q={{event.location.address|urlencode}}&z=16" target="gmaps"><img src="{{ STATIC_URL }}img/google_maps.png" alt="Google Maps" title="{% trans 'Show on Google Maps' %}" width="39" height="39"/></a>
|
||||
</div>
|
||||
<br class="clear" />
|
||||
{% render_comment_list for event %}
|
||||
{% render_comment_form for event %}
|
||||
{% endblock %}
|
||||
|
||||
{% block buttonbar %}
|
||||
{% if perms.events.change_event %}
|
||||
<a class="button" href="{{ event.get_edit_url }}"><img src="{{ STATIC_URL }}icons/page_edit.png" alt="" /> {% trans "Edit Event" %}</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
var geocoder;
|
||||
var map;
|
||||
function codeAddress() {
|
||||
var address = "{{ event.location.address }}";
|
||||
geocoder.geocode( { 'address': address}, function(results, status) {
|
||||
if (status == google.maps.GeocoderStatus.OK) {
|
||||
map.setCenter(results[0].geometry.location);
|
||||
var marker = new google.maps.Marker({
|
||||
map: map,
|
||||
position: results[0].geometry.location
|
||||
});
|
||||
} else {
|
||||
alert("Geocode was not successful for the following reason: " + status);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
geocoder = new google.maps.Geocoder();
|
||||
var latlng = new google.maps.LatLng(-34.397, 150.644);
|
||||
var mapOptions = {
|
||||
zoom: 16,
|
||||
center: latlng,
|
||||
mapTypeId: google.maps.MapTypeId.ROADMAP
|
||||
}
|
||||
map = new google.maps.Map(document.getElementById("map_canvas"), mapOptions);
|
||||
codeAddress();
|
||||
}
|
||||
initialize();
|
||||
{% endblock %}
|
||||
49
src/templates/events/event_form.html
Normal file
49
src/templates/events/event_form.html
Normal file
@@ -0,0 +1,49 @@
|
||||
{% extends "events/page.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block extra_head %}
|
||||
<link rel="stylesheet" href="{{STATIC_URL}}css/jquery-ui-1.8.16.custom.css" type="text/css">
|
||||
<script type="text/javascript" src="{{STATIC_URL}}js/jquery-ui-1.8.16.custom.min.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block maincontent %}
|
||||
<form method="post" enctype="multipart/form-data" class="grid_12">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
<legend>{% if event.pk %}{% trans "Edit Event"%}{% else %}{% trans "Add Event"%}{% endif %}</legend>
|
||||
{% include "form.html" %}
|
||||
{% if event.id and event.event_set.count %}
|
||||
<p class="warning">
|
||||
<strong>Achtung! Das ist eine Veranstaltungsreihe!</strong> Diese kann man im Moment nur im Admin-Interface vernünfig bearbeiten.<br />
|
||||
Du bearbeitest hier den "Hauptevent" der Reihe ({{event.event_set.count}}). Alle Änderungen (abgesehen von Name, Start und Ende) werden von den darauf folgendem Veranstaltungen übernommen.
|
||||
</strong>
|
||||
</p>
|
||||
{% endif %}
|
||||
<p class="buttonbar">
|
||||
<button type="reset"><img src="{{STATIC_URL}}icons/arrow_undo.png" alt="{% trans 'reset' %}" /> {% trans 'reset' %}</button>
|
||||
<button type="submit"><img src="{{STATIC_URL}}icons/disk.png" alt="{% trans 'save' %}" /> {% trans 'save' %}</button>
|
||||
</p>
|
||||
</fieldset>
|
||||
|
||||
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block buttonbar %}{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
$(function() {
|
||||
$.datepicker.setDefaults($.datepicker.regional['{{LANGUAGE_CODE}}']);
|
||||
$( "#id_start_0" ).datepicker({
|
||||
changeMonth: true,
|
||||
changeYear: true,
|
||||
dateFormat: 'yy-mm-dd'
|
||||
});
|
||||
$( "#id_end_0" ).datepicker({
|
||||
changeMonth: true,
|
||||
changeYear: true,
|
||||
dateFormat: 'yy-mm-dd',
|
||||
});
|
||||
});
|
||||
|
||||
{% endblock %}
|
||||
45
src/templates/events/event_list.html
Normal file
45
src/templates/events/event_list.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{% extends "events/event_archive.html" %}
|
||||
{% load i18n comments%}
|
||||
|
||||
{% block title %}{% trans "Upcoming Events" %}{% endblock %}
|
||||
|
||||
{% block maincontent %}
|
||||
<h2 class="grid_12">{% trans "Upcoming Events" %}</h2>
|
||||
{% for event in event_list %}
|
||||
{% ifchanged %}<h3 class="grid_12">{{ event.start|date:'F Y' }}</h3>{% endifchanged %}
|
||||
{% get_comment_count for event as comment_count %}
|
||||
<div class="grid_6">
|
||||
<a href="{{ event.get_absolute_url }}"><img src="{{ event.get_thumbnail.url }}" alt=" {% trans 'Event Image' %}" class="thumbnail"/></a>
|
||||
<h4><a href="{{ event.get_absolute_url }}">{{ event.name }}</a></h4>
|
||||
{{event.description}}
|
||||
<ul class="info">
|
||||
<li>
|
||||
<img src="{{ STATIC_URL }}/icons/date.png" alt="{% trans 'Start' %}:" title="{% trans 'Start' %}">
|
||||
{{ event.start|date:'SHORT_DATE_FORMAT' }}
|
||||
{% if event.end %}
|
||||
{% trans "from" %} {{ event.start|time:'H:i' }} {% trans "to" %} {{ event.end|time:'H:i' }}
|
||||
{% else %}
|
||||
{{ event.start|time:'H:i' }}
|
||||
{% endif %}
|
||||
</li>
|
||||
<li>
|
||||
<img src="{{ STATIC_URL }}/icons/map.png" alt="{% trans 'Location' %}" title="{% trans 'Location' %}">
|
||||
{{ event.location }}
|
||||
</li>
|
||||
<li>
|
||||
<img src="{{ STATIC_URL }}/icons/comments.png" alt="{% trans 'Comments' %}" title="{% trans 'Comments' %}">
|
||||
<a href="{{event.get_absolute_url}}#comments">{{ comment_count }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<p style="text-align:right">
|
||||
{% if perms.events.change_event %}
|
||||
<a href="{{ event.get_edit_url }}" class="button"><img src="{{ STATIC_URL }}icons/page_edit.png" alt="{%trans "Edit" %}"></a>
|
||||
{% endif %}
|
||||
{% if perms.events.add_photo %}
|
||||
<a href="{% url 'event-photo-list' event.pk %}" class="button"><img src="{{ STATIC_URL }}icons/image_add.png" alt="{%trans "Upload" %}"></a>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if page_obj.has_other_pages %}{% include 'paginator.html' %}{% endif %}
|
||||
{% endblock %}
|
||||
47
src/templates/events/event_site.html
Normal file
47
src/templates/events/event_site.html
Normal file
@@ -0,0 +1,47 @@
|
||||
{% extends "events/page.html" %}
|
||||
{% load i18n django_markdown%}
|
||||
|
||||
{% block title %}{{ event.name }}{% endblock %}
|
||||
|
||||
{% block opengraph %}
|
||||
<meta property="og:type" content="activity" />
|
||||
<meta property="og:title" content="{{event.name}}" />
|
||||
<meta property="og:url" content="http://www.kasu.at{{event.get_absolute_url}}" />
|
||||
<meta property="og:image" content="http://www.kasu.at{{ event.get_thumbnail.url }}" />
|
||||
{% if event.description %}<meta property="og:description" content="{{event.description}}" />{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<style type="text/css" media="screen and (min-width: 701px)">#teaser{background-image:url('{{ event.get_callout.url }}')}</style>
|
||||
<!--[if lt IE 9]><style type="text/css" media="screen">#teaser{background-image:url("{{event.get_callout.url}}")}</style><![endif]-->
|
||||
{% endblock %}
|
||||
|
||||
{% block maincontent %}
|
||||
<div class="grid_8" id="teaser">
|
||||
<div id="teaser_text">
|
||||
<h2>{{event.name}}</h2>
|
||||
{% if event.description %}{{event.description|markdown}}{% else %}{{event.location.description|markdown}}{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="red_box grid_4">
|
||||
{% block red_box %}
|
||||
<h2>Info</h2>
|
||||
<p> </p>
|
||||
<ul>
|
||||
<li class="date"><strong>{% trans "Start" %}:</strong> {{ event.start }}</li>
|
||||
{% if event.end %}<li class="date"><strong>{% trans "End" %}:</strong> {{ event.end }}</li>{% endif %}
|
||||
<li class="location">
|
||||
{% if event.location.url %}<a href="{{ event.location.url }}">{% else %}<a href="http://maps.google.com/maps?q={{event.location.address|urlencode}}&z=16">{% endif %}
|
||||
<strong>{% trans "Location" %}:</strong> {{event.location.name}}</a>
|
||||
</li>
|
||||
{% if event.url %}<li><a href="{{ event.url }}"><strong>{% trans "Homepage" %}:</strong> {{ event.url }}</a></li>{% endif %}
|
||||
<li class="hanchan"><a href="{% url 'event-hanchan-list' event.pk %}" ><strong>{% trans "Hanchans" %}:</strong> {{ event.hanchan_set.count }}</a></li>
|
||||
<li class="photo"><a href="{% url 'event-photo-list' event.pk %}"><strong>{% trans 'Photos' %}:</strong> {{ event.photo_count }}</a></li>
|
||||
{% if event.is_tournament %}<li class="season"><a href="{% url 'event-ranking' event.pk %}"><strong>{% trans "tourney" %}:</strong> {% trans "other rules apply here" %}</a></li>{% endif%}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% block event_content %} {% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
24
src/templates/events/page.html
Normal file
24
src/templates/events/page.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
{% load comments i18n %}
|
||||
|
||||
{% block title %}{{page.title}}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>{{ page.title }}</h2>
|
||||
{{ page.content }}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
<a href="http://www.google.com/calendar/render?cid=http%3A%2F%2Fwww.kasu.at%2Fevents.ics" target="_blank"><img src="http://www.google.com/calendar/images/ext/gc_button6.gif" alt="0" border="0"></a>
|
||||
{% endblock %}
|
||||
|
||||
{% block additional_buttonbar %}
|
||||
{% if perms.events.add_event %}
|
||||
<a href="{% url 'event-form' %}" class="button"><img src="{{ STATIC_URL }}icons/calendar_add.png" alt=""/> {% trans 'Add Event' %}</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
21
src/templates/events/photo_confirm_delete.html
Normal file
21
src/templates/events/photo_confirm_delete.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n comments %}
|
||||
|
||||
{% block maincontent %}
|
||||
<header>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<h1 class="grid_12">Dieses Photo wirklich löschen?</h1>
|
||||
</header>
|
||||
<img src="{{photo.display.url}}" alt="{{photo.name}}" title="{{photo.name}}" class="grid_10 push_1" />
|
||||
<br class="clear" />
|
||||
<p>Sind Sie sicher, dass Sie das Bild “{{photo.name}}” löschen wollen?</p>
|
||||
<p class="buttonbar">
|
||||
<a href="{% url 'event-photo-list' photo.event.id %}" class="button" style="float: left;"><img src="{{STATIC_URL}}icons/cancel.png" alt="{% trans 'Cancel' %}" /> {% trans 'Cancel' %}</a>
|
||||
<button type="submit"><img src="{{STATIC_URL}}icons/delete.png" alt="{% trans 'Delete' %}" /> {% trans 'Delete' %}</button>
|
||||
</form>
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
56
src/templates/events/photo_detail.html
Normal file
56
src/templates/events/photo_detail.html
Normal file
@@ -0,0 +1,56 @@
|
||||
{% extends "events/photo_gallery.html" %}
|
||||
{% load i18n comments %}
|
||||
|
||||
{% block title %} {{ photo.name }} - {{ photo.event.name }} {% endblock %}
|
||||
|
||||
{% block opengraph %}
|
||||
<meta property="og:type" content="photo" />
|
||||
<meta property="og:title" content="{{photo.name}} - Foto" />
|
||||
<meta property="og:url" content="http://www.kasu.at{{photo.get_absolute_url}}" />
|
||||
<meta property="og:image" content="http://www.kasu.at{{photo.thumbnail.url}}" />
|
||||
{% if photo.description %}<meta property="og:description" content="{{photo.description}}" />{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block maincontent %}
|
||||
<h2 class="grid_12"><a href="{% url 'event-photo-list' photo.event.id %}">{{photo.event.name}}</a> » {{ photo.name }}</h2>
|
||||
<div id="display" class="grid_12 clearfix">
|
||||
<img src="{{photo.display.url}}" alt="{{photo.name}}" title="{{photo.name}}"/>
|
||||
{% if photo.previous_photo %}
|
||||
<a href="{{ photo.previous_photo.get_absolute_url }}" class="previous">{% trans 'previous' %}</a>
|
||||
{% endif %}
|
||||
{% if photo.next_photo %}
|
||||
<a href="{{ photo.next_photo.get_absolute_url }}" class="next">Next</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if perms.events.change_photo %}
|
||||
<form method="post" enctype="multipart/form-data" class="grid_12">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
{% include "form.html" %}
|
||||
<p class="buttonbar">
|
||||
<a href="{{ photo.image.url }}" class="button" type="application/octet-stream"><img src="{{ STATIC_URL}}icons/drive_go.png" alt="{% trans 'download' %}" title="{% trans 'download' %}" /></a>
|
||||
<button type="submit" name="rotate" value="counter-clockwise"><img src="{{STATIC_URL}}icons/shape_rotate_anticlockwise.png" title="Gegen den Uhrzeiger drehen"></button>
|
||||
<button type="submit" name="rotate" value="clockwise"><img src="{{STATIC_URL}}icons/shape_rotate_clockwise.png" title="Im Uhrzeiger drehen"></button>
|
||||
<button type="submit"><img src="{{STATIC_URL}}icons/disk.png" alt=""> {% trans "save" %}</button>
|
||||
</p>
|
||||
</fieldset>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="grid_10 push_1">{{ photo.description }}</p>
|
||||
|
||||
<ul class="info grid_11 push_1">
|
||||
<li class="user"><strong>{% trans 'Photographer' %}: </strong>{{ photo.photographer }}</li>
|
||||
<li class="date"><strong>{% trans 'on' %}</strong> {{ photo.created_date }}</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
<div class="grid_12 more_link">
|
||||
<a href="https://m.google.com/app/plus/x/?v=compose&content={{photo.headline|urlencode}}+-+http%3A%2F%2Fwww.kasu.at/{{photo.get_absolute_url|urlencode}}" target="_blank"><img src="{{STATIC_URL}}img/google_plus.png" alt="Google+" title="{% trans 'Share on Google+'%}" /></a>
|
||||
<a href="https://twitter.com/share?url=http%3A%2F%2Fwww.kasu.at/{{photo.get_absolute_url|urlencode}}" target='_blank'><img src="{{STATIC_URL}}img/twitter.png" alt="Twitter" title="{% trans 'Share on Twitter' %}" /></a>
|
||||
<a href="http://facebook.com/sharer.php?u=http%3A%2F%2Fwww.kasu.at{{photo.get_absolute_url|urlencode}}" target="_blank"><img src="{{STATIC_URL}}img/facebook.png" alt="Facebook" title="{% trans 'Share on Facebook'%}" /></a>
|
||||
</div>
|
||||
{% render_comment_list for photo %}
|
||||
{% render_comment_form for photo %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
36
src/templates/events/photo_gallery.html
Normal file
36
src/templates/events/photo_gallery.html
Normal file
@@ -0,0 +1,36 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block navigation %}
|
||||
<ul id="navigation">
|
||||
{% if event %}
|
||||
<li><a href="{{ current_top_page.get_absolute_url }}">{{current_top_page.menu_name}}</a></li>
|
||||
<li><a class="active">{{event.name}}</a></li>
|
||||
{% elif photo %}
|
||||
<li><a href="{{ current_top_page.get_absolute_url }}">{{current_top_page.menu_name}}</a></li>
|
||||
<li><a href="{% url 'event-photo-list' photo.event.id %}">{{photo.event.name}}</a></li>
|
||||
<li><a class="active">{{photo.name}}</a></li>
|
||||
{% else %}
|
||||
<li><a href="{{ current_top_page.get_absolute_url }}" {% ifequal current_page current_top_page %}class="active"{% endifequal %}>{{current_top_page.menu_name}}</a></li>
|
||||
{% if perms.event.add_photo %}
|
||||
<li><a href="/gallery/upload/" {% ifequal current_path 'gallery/upload' %}class="active"{% endifequal %}>{% trans 'Upload' %}</a></li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
{% block maincontent %}
|
||||
<h2 class="grid_12">{% trans 'Photos' %}</h2>
|
||||
{% for event in event_list %}
|
||||
<div class="gallery grid_4">
|
||||
<h3><a href="{% url 'event-photo-list' event.id %}">{{event.name}}</a></h3>
|
||||
<a href="{% url 'event-photo-list' event.id %}"><img src="{{event.get_thumbnail.url}}" class="thumbnail"/></a>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p>Sorry da kommt erst was hin!</p>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block buttonbar %}{% endblock %}
|
||||
|
||||
|
||||
68
src/templates/events/photo_list.html
Normal file
68
src/templates/events/photo_list.html
Normal file
@@ -0,0 +1,68 @@
|
||||
{% extends "events/event_site.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{{event.name}}{% endblock %}
|
||||
|
||||
{% block opengraph %}
|
||||
<meta property="og:type" content="album" />
|
||||
<meta property="og:title" content="{{event.name}}" />
|
||||
<meta property="og:url" content="http://www.kasu.at{% url 'event-photo-list' event.pk %}" />
|
||||
<meta property="og:image" content="http://www.kasu.at{{ event.get_thumbnail.url }}" />
|
||||
{% if event.description %}<meta property="og:description" content="{{event.description}}" />{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block navigation %}
|
||||
<ul id="navigation">
|
||||
{% if event %}
|
||||
<li><a href="{{ current_top_page.get_absolute_url }}">{{current_top_page.menu_name}}</a></li>
|
||||
<li><a class="active">{{event.name}}</a></li>
|
||||
{% elif photo %}
|
||||
<li><a href="{{ current_top_page.get_absolute_url }}">{{current_top_page.menu_name}}</a></li>
|
||||
<li><a href="{% url 'event-photo-list' photo.event.id %}">{{photo.event.name}}</a></li>
|
||||
<li><a class="active">{{photo.name}}</a></li>
|
||||
{% else %}
|
||||
<li><a href="{{ current_top_page.get_absolute_url }}" {% ifequal current_page current_top_page %}class="active"{% endifequal %}>{{current_top_page.menu_name}}</a></li>
|
||||
{% if perms.event.add_photo %}
|
||||
<li><a href="/gallery/upload/" {% ifequal current_path 'gallery/upload' %}class="active"{% endifequal %}>{% trans 'Upload' %}</a></li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block event_content %}
|
||||
{% if perms.events.delete_photo %}
|
||||
{% for photo in photo_list %}
|
||||
<div class="thumbnail">
|
||||
<a href="{{photo.get_absolute_url}}"><img src="{{photo.thumbnail.url}}" alt=""/></a>
|
||||
<a href="{% url 'delete-event-photo' photo.pk %}" class="delete_image"><img src="{{STATIC_URL}}icons/delete.png" title="{% trans 'delete' %}"/></a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for photo in photo_list %}
|
||||
<a href="{{photo.get_absolute_url}}" class="thumbnail"><img src="{{photo.thumbnail.url}}" alt=""/></a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if perms.events.add_photo %}
|
||||
<br class="clear" />
|
||||
<form action="{% url 'event-photo-upload' event.id %}" method="post" enctype="multipart/form-data" class="grid_12">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
<legend>Photos hochladen</legend>
|
||||
{% include "form.html"%}
|
||||
<p class="buttonbar">
|
||||
<button type="reset"><img src="{{STATIC_URL}}icons/arrow_undo.png" alt="{% trans 'reset' %}" /> {% trans 'reset' %}</button>
|
||||
<button type="submit"><img src="{{STATIC_URL}}icons/image_add.png" alt="{% trans 'upload' %}" /> {% trans 'upload' %}</button>
|
||||
</p>
|
||||
</fieldset>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if page_obj.has_other_pages %}{% include 'paginator.html' %}{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block buttonbar %}{% endblock %}
|
||||
|
||||
|
||||
54
src/templates/events/photo_upload.html
Normal file
54
src/templates/events/photo_upload.html
Normal file
@@ -0,0 +1,54 @@
|
||||
{% extends "events/photo_gallery.html" %}
|
||||
{% load i18n comments %}
|
||||
|
||||
{% block maincontent %}
|
||||
<h2 class="grid_12">Photos</a> » {% trans "upload"%}</h2>
|
||||
{% for event in event_list %}
|
||||
{% get_comment_count for event as comment_count %}
|
||||
{% ifchanged %}<h3 class="grid_12">{{ event.start|date:'F Y' }}</h3>{% endifchanged %}
|
||||
<div style="float:left">
|
||||
<a href="{% url 'event-photo-list' event.pk %}"><img src="{{ event.get_thumbnail.url }}" alt="" class="thumbnail"/></a>
|
||||
<div class="grid_4" />
|
||||
<h4><a href="{% url 'event-photo-list' event.pk %}">{{ event.name }}</a></h4>
|
||||
<div class="info">
|
||||
<img src="{{ STATIC_URL }}/icons/date.png" alt="{% trans 'Start' %}" title="{% trans 'Start' %}">
|
||||
{{ event.start|date }}
|
||||
{% if event.end %}
|
||||
{% trans "from" %} {{ event.start|time:'H:i' }} {% trans "to" %} {{ event.end|time:'H:i' }}
|
||||
{% else %}
|
||||
{{ event.start|time:'H:i' }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if event.description %}<p>{{event.description}}</p>{% endif %}
|
||||
<div class="info">
|
||||
<img src="{{ STATIC_URL }}/icons/map.png" alt="{% trans 'Location' %}" title="{% trans 'Location' %}">
|
||||
{{ event.location }}
|
||||
<img src="{{ STATIC_URL }}/icons/comments.png" alt="{% trans 'Comments' %}" title="{% trans 'Comments' %}">
|
||||
<a href="{{event.get_absolute_url}}#comments">{{ comment_count }} {% trans 'Comments' %}</a>
|
||||
<img src="{{ STATIC_URL }}/icons/images.png" alt="{% trans 'Photos' %}" title="{% trans 'Photos' %}">
|
||||
<a href="{% url 'event-photo-list' event.pk %}">{{ event.photo_count }} {% trans 'Photos' %}</a>
|
||||
</div>
|
||||
<p style="text-align:right">
|
||||
{% if perms.events.add_photo %}
|
||||
<a href="{% url 'event-photo-list' event.pk %}" class="button"><img src="{{ STATIC_URL }}icons/image_add.png" alt="{%trans "Upload" %}"></a>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div></div>
|
||||
{% endfor %}
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<fieldset class="grid_8 push_2">
|
||||
<legend>Photos hochladen</legend>
|
||||
{% include "form.html" %}
|
||||
<p class="buttonbar">
|
||||
<button type="submit">
|
||||
<img src="{{ STATIC_URL }}icons/drive_go.png" alt="" />
|
||||
{% trans "upload" %}
|
||||
</button>
|
||||
</p>
|
||||
</fieldset>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
9
src/templates/form.html
Normal file
9
src/templates/form.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{% for field in form.visible_fields %}
|
||||
<div>
|
||||
<label for="id_{{ field.html_name}}" class="field_name {{ field.css_classes }}">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
{%if field.help_text %}<p class="help_text">{{field.help_text}}</p>{% endif %}
|
||||
{%if field.errors %}{{ field.errors }}{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %}
|
||||
1
src/templates/google25dabc1a49a9ef03.html
Normal file
1
src/templates/google25dabc1a49a9ef03.html
Normal file
@@ -0,0 +1 @@
|
||||
google-site-verification: google25dabc1a49a9ef03.html
|
||||
94
src/templates/index.html
Normal file
94
src/templates/index.html
Normal file
@@ -0,0 +1,94 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n comments%}
|
||||
|
||||
{% block extra_head %}
|
||||
<style type="text/css" media="screen and (min-width: 701px)">
|
||||
#teaser{background-image:url("{{random_photo.url}}")}</style>
|
||||
<!--[if lt IE 9]><style type="text/css" media="screen">#teaser{background-image:url("{{random_photo.url}}")}</style><![endif]-->
|
||||
{% endblock %}
|
||||
|
||||
{% block maincontent %}
|
||||
<section id="teaser" class="grid_8">
|
||||
<div id="teaser_text"><h2>{{title}}</h2>{{content}}</div>
|
||||
</section>
|
||||
<aside id="next_event" class="grid_4 red_box" itemscope itemtype="http://schema.org/Event">
|
||||
{% if current_event %}
|
||||
<h2>{% trans "Current Event" %}</h2>
|
||||
<h3>{{ current_event.name}}</h3>
|
||||
<p><img src="{{ STATIC_URL }}icons/date.png" /> {% trans "since" %} <time datetime="{{current_event.start|date:'c'}}">{{current_event.start|timesince}}</time></p>
|
||||
<ul class="list">
|
||||
<li class="event"><strong>{% trans "Start" %}:</strong> {{current_event.start|date:'DATETIME_FORMAT'}}</li>
|
||||
<li class="location"><strong>{% trans "Location" %}:</strong> {{ current_event.location }}</li>
|
||||
</ul>
|
||||
<div style="text-align:right"><a class="button" href="{{current_event.get_absolute_url}}">{% trans "More Details" %} <img src="{{ STATIC_URL }}icons/date_go.png" alt="»" width="16" height="16"/></a></div>
|
||||
{% else %}
|
||||
<h2>{% trans "Next Event" %}</h2>
|
||||
<h3>{{ next_event.name}}</h3>
|
||||
<p><img src="{{ STATIC_URL }}icons/date.png" alt="" width="16" height="16"/> {% trans "in" %} <time datetime="{{next_event.start|date:'c'}}">{{next_event.start|timeuntil}}</time></p>
|
||||
<ul class="list">
|
||||
<li class="event"><strong>{% trans "Start" %}:</strong> {{next_event.start|date:'DATETIME_FORMAT' }}</li>
|
||||
<li class="location"><strong>{% trans "Location" %}:</strong> {{ next_event.location }}</li>
|
||||
</ul>
|
||||
<div style="text-align:right"><a class="button" href="{{next_event.get_absolute_url}}">{% trans "More Details" %} <img src="{{ STATIC_URL }}icons/date_go.png" alt="»" width="16" height="16"/></a></div>
|
||||
{% endif %}
|
||||
<h3 class="clearfix">{% trans "Upcoming events" %} <a href="{% url 'events-ical' %}"><img src="{{STATIC_URL}}img/ical_feed.gif" alt="iCal Feed" title="iCal Feed" width="16" height="16"/></a></h3>
|
||||
<ul class="list">
|
||||
{% for event in upcoming_events %}
|
||||
<li class="event">{{event.start|date:'d. M:'}} <a href="{{ event.get_absolute_url}}">{{event.name}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</aside>
|
||||
<br class="clear" />
|
||||
|
||||
<section class="grid_8">
|
||||
{% for article in recent_article_list %}{% get_comment_count for article as comment_count %}
|
||||
<article class="article">
|
||||
<header>
|
||||
<h2><a href="{{article.get_absolute_url}}">{{article.headline}}</a></h2>
|
||||
<ul class="info">
|
||||
<li><img src="{{STATIC_URL}}icons/date.png" width="16" height="16" alt="{% trans 'Created on' %}" title="{% trans 'Created on' %}"/> <time date="{{article.date_created|date:'c'}}">{{ article.date_created|date }}</time></li>
|
||||
<li><img src="{{STATIC_URL}}icons/user_red.png" width="16" height="16" alt="{% trans 'Author' %}" title="{% trans 'Author' %}"/> {{ article.author }}</li>
|
||||
<li><img src="{{STATIC_URL}}icons/comments.png" alt="{% trans 'comments' %}:" title="{% trans 'comments' %}"/> <a href="{{article.get_absolute_url}}#comments" >{{comment_count}} {% trans "comments" %}</a></li>
|
||||
</ul>
|
||||
</header>
|
||||
<a href="{{article.get_absolute_url}}"><img src="{{article.posting_image.url}}" alt="{{article.category}}:" class="posting_image"/></a>
|
||||
{{article.content|truncatewords_html:50}}
|
||||
<footer class="more_link"><a href="{{article.get_absolute_url}}" class="button">{% trans "Read More"%} <img src="{{STATIC_URL}}icons/page_go.png" alt="»" width="16" height="16"/></a></footer>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
<aside class="grid_4">
|
||||
<section>
|
||||
<h2>{% trans "Recent Comments" %} <a href="{% url 'feed-latest-comments' %}"><img src="{{ STATIC_URL }}img/rss_feed.gif" alt="RSS Feed" title="RSS Feed" width="16" height="16"/></a></h2>
|
||||
<ul class="comment_list">
|
||||
{% for comment in recent_comment_list %}
|
||||
{% url 'membership-details' comment.user.username as user_link%}
|
||||
<li>
|
||||
{% blocktrans with comment.user as author and comment.submit_date|timesince as since and comment.submit_date|date:'c' as submit_date and comment.content_object as object and comment.get_absolute_url as comment_link%}
|
||||
From <a href="{{user_link}}">{{author}}</a> in
|
||||
<a href="{{comment_link}}">“{{object}}”</a>
|
||||
since <time datetime="{{submit_date}}">{{since}}</time>
|
||||
{% endblocktrans %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>{% trans 'Kasu in the social network' %}</h2>
|
||||
<p style="text-align: center">
|
||||
<a href="https://www.facebook.com/Kasu.at"><img src="{{STATIC_URL}}img/facebook.png" alt="Facebook" title="{% trans 'Visit us on' %} Facebook" /></a>
|
||||
<a href="https://twitter.com/KasuAustria"><img src="{{STATIC_URL}}img/twitter.png" alt="Twitter" title="{% trans 'Visit us on' %} Twitter" /></a>
|
||||
<a href="https://plus.google.com/u/0/114092233962732014973/"><img src="{{STATIC_URL}}img/google_plus.png" alt="Google+" title="{% trans 'Visit us on' %} Google+" /></a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
||||
</aside>
|
||||
{% endblock %}
|
||||
|
||||
{% if perms.content.add_article %}{% block addidional_buttonbar %}
|
||||
<a href="{% url 'add-article' %}" class="button"><img src="{{ STATIC_URL }}icons/note_add.png" alt="" width="16" height="16"/>{% trans "Add Article" %}</a>
|
||||
{% endblock %}{% endif %}
|
||||
|
||||
83
src/templates/mahjong_ranking/eventranking_list.html
Normal file
83
src/templates/mahjong_ranking/eventranking_list.html
Normal file
@@ -0,0 +1,83 @@
|
||||
{% extends "events/event_site.html" %}
|
||||
|
||||
{% load i18n comments%}
|
||||
|
||||
{% block title %}{% trans "Tournament Ranking" %}: {{ event.name }}{% endblock %}
|
||||
|
||||
{% block event_content %}
|
||||
|
||||
{% if event.is_tournament %}
|
||||
<ul class="tabs grid_12">
|
||||
<li><a href="{% url 'event-hanchan-list' event.id %}">{% trans "Tournament Hanchans" %}</a></li>
|
||||
<li class="active"><a href="{% url 'event-ranking' event.id %}">{% trans "Tournament Ranking" %}</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
<div class="grid_12">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="2">{% trans "Rank" %}</th>
|
||||
<th rowspan="2">{% trans "Avatar" %}</th>
|
||||
<th rowspan="2">{% trans "Nickname" %}</th>
|
||||
<th rowspan="2">{% trans "Name" %}</th>
|
||||
<th colspan="2">{% trans 'Average' %}</th>
|
||||
<th colspan="2">Hanchans</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans 'Placement' %}</th>
|
||||
<th>{% trans "Score" %}</th>
|
||||
<th>{% trans "won" %}</th>
|
||||
<th>{% trans "good" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for player in eventranking_list %}
|
||||
{% with player.user.get_profile as profile %}
|
||||
<tr>
|
||||
<td class="center">{{player.placement}}.</td>
|
||||
<td class="avatar"><a href="{{ player.user.get_absolute_url }}">
|
||||
{% if profile.thumbnail %}
|
||||
<img src="{{profile.thumbnail.url}}" alt="" />
|
||||
{% else %}
|
||||
<img src="{{STATIC_URL}}img/unknown_thumbnail.png" alt=""/>
|
||||
{% endif %}
|
||||
</a></td>
|
||||
<td><a href="{{ player.user.get_absolute_url }}">{{player.user}}</a></td>
|
||||
<td>{{profile.last_name}} {{profile.first_name}}</td>
|
||||
<td class="center">{{player.avg_placement|floatformat:2 }}</td>
|
||||
<td class="right">{{player.avg_score|floatformat:0 }}</td>
|
||||
<td class="center">{{player.won_hanchans}}</td>
|
||||
<td class="center">{{player.good_hanchans}}</td>
|
||||
</tr>
|
||||
{% endwith %}
|
||||
{% empty %}
|
||||
<tr><td colspan="8">Leider hat es noch niemand in das Ranking geschafft.
|
||||
Ein Spieler wird erst ins Ranking genommen wenn er 5 Hanchans absolviert hat.</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% if is_archive %}
|
||||
<h2>{% trans 'Ladder Archive' %}</h2>
|
||||
<ul class="list">
|
||||
{% for season in season_archive %}
|
||||
<li class="season"><a href="{% url 'mahjong-ladder-archive' season.id %}">{{season.name}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<h2>{% trans 'Latest Events' %}</h2>
|
||||
<ul class="list">
|
||||
{% for event in latest_event_list %}
|
||||
<li class="event"><a href="{% url 'event-hanchan-list' event.pk %}">{{event.name}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h2>{% trans 'Latest Hanchans' %}</h2>
|
||||
<ul class="comment_list">
|
||||
{% for hanchan in latest_hanchan_list %}
|
||||
<li class="hanchan"><a href="{% url 'event-hanchan-list' hanchan.event.pk %}">{{hanchan.event.name}}</a> {{hanchan.start|time:'H:i'}}: {{hanchan.player_names}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
26
src/templates/mahjong_ranking/hanchan_confirm_delete.html
Normal file
26
src/templates/mahjong_ranking/hanchan_confirm_delete.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n comments %}
|
||||
|
||||
{% block meta_title %}{% trans 'Delete Hanchan' %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
<legend>{% trans "Delete Hanchan" %}</legend>
|
||||
{% include 'form.html' %}
|
||||
<p class="buttonbar">
|
||||
<button type="button" onclick="window.history.back()">
|
||||
<img src="{{STATIC_URL}}icons/cancel.png" alt="{% trans 'Cancel' %}"/>
|
||||
{% trans 'Cancel' %}
|
||||
</button>
|
||||
<button type="submit">
|
||||
<img src="{{STATIC_URL}}icons/table_delete.png" alt="{% trans 'Delete' %}"/>
|
||||
{% trans 'Delete' %}
|
||||
</button>
|
||||
</p>
|
||||
</fieldset>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block buttonbar %}{% endblock %}
|
||||
59
src/templates/mahjong_ranking/hanchan_form.html
Normal file
59
src/templates/mahjong_ranking/hanchan_form.html
Normal file
@@ -0,0 +1,59 @@
|
||||
{% extends "events/event_site.html" %}
|
||||
{% load i18n comments fieldset_extras %}
|
||||
|
||||
{% block title %}
|
||||
{% if hanchan.id %}{% trans "Edit Hanchan" %}{% else %}{% trans "Add Hanchan" %}{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block event_content %}
|
||||
{% get_fieldset "event, start" from form as event_formset %}
|
||||
{% if perms.mahjong_ranking.delete_hanchan %}
|
||||
{% get_fieldset "comment, confirmed" from form as hanchan_formset %}
|
||||
{% else %}
|
||||
{% get_fieldset "comment" from form as hanchan_formset %}
|
||||
{% endif %}
|
||||
{% for hidden in form.hidden_fields %}{% if hidden.errors %}{{ hidden.errors }}{% endif %}{% endfor %}
|
||||
|
||||
<form class="grid_12" method="post">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
<legend>{% if hanchan.id %}{% trans "Edit Hanchan" %}{% else %} {% trans "Add Hanchan" %}{% endif %}</legend>
|
||||
{% with event_formset as form %}{% include "form.html" %}{% endwith %}
|
||||
<div>
|
||||
<label class="field_name required">{% trans 'Players' %}</label>
|
||||
{{ formset.management_form }}
|
||||
<table>
|
||||
<tr>
|
||||
<th>{% trans 'User' %}</th>
|
||||
<th>{% trans 'Score' %}</th>
|
||||
<th>{% trans 'Bonus' %}</th>
|
||||
<th width="75%">{% trans 'Comment' %}</th>
|
||||
</tr>
|
||||
{% for form in formset %}
|
||||
<tr>
|
||||
<td class="{{form.user.css_classes}}">{{form.id}} {{form.user}} {% for error in form.user.errors %}<br />{{error}}{% endfor %}</td>
|
||||
<td class="{{form.score.css_classes}}">{{form.score}} {% for error in form.score.errors %}<br />{{error}}{% endfor %}</td>
|
||||
<td class="{{form.bonus_points.css_classes}}">{{form.bonus_points}} {% for error in form.bonus_points.errors %}<br />{{error}}{% endfor %}</td>
|
||||
<td class="{{form.comment.css_classes}}" width="*">{{form.comment}} {% for error in form.comment.errors %}<br />{{error}}{% endfor %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% for error in formset.errors %}<p class="error">{{error}}</p>{% endfor %}
|
||||
|
||||
{% if hanchan.pk and not hanchan.valid %}<p class="error">{{hanchan.check_validity}}</p>{% endif %}
|
||||
|
||||
{% with hanchan_formset as form %}{% include "form.html" %}{% endwith %}
|
||||
{% for error in form.non_field_errors %}<p class="error">{{error}}</p>{% endfor %}
|
||||
|
||||
|
||||
<p class="buttonbar">
|
||||
<a href="{{hanchan.get_absolute_url}}" class="button"><img src="{{STATIC_URL}}icons/arrow_undo.png" alt="{% trans 'back' %}" /> {% trans 'back' %}</a>
|
||||
<button type="submit"><img src="{{STATIC_URL}}icons/table_save.png" alt="{% trans 'save' %}" /> {% trans 'save' %}</button>
|
||||
</p>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block buttonbar %}{% endblock %}
|
||||
53
src/templates/mahjong_ranking/hanchan_list.html
Normal file
53
src/templates/mahjong_ranking/hanchan_list.html
Normal file
@@ -0,0 +1,53 @@
|
||||
{% extends "events/event_site.html" %}
|
||||
|
||||
{% load i18n comments %}
|
||||
|
||||
{% block title %}Hanchans: {{ event.name }}{% endblock %}
|
||||
|
||||
{% block event_content %}
|
||||
|
||||
{% if event.is_tournament %}
|
||||
<ul class="tabs grid_12">
|
||||
<li class="active"><a href="{% url 'event-hanchan-list' event.id %}">{% trans "Tournament Hanchans" %}</a></li>
|
||||
<li><a href="{% url 'event-ranking' event.id %}">{% trans "Tournament Ranking" %}</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% for hanchan in hanchan_list %}
|
||||
<h3 class="grid_12 clearfix" id="{{ hanchan.pk }}">{{hanchan.start|time:'H:i'}}: {{ hanchan.player_names }}</h3>
|
||||
{% for player in hanchan.player_set.all %}
|
||||
<div class="player" >
|
||||
<a href="{% url 'player-ladder-score' player.user %}"><img
|
||||
{% if player.user.get_profile.thumbnail %}src="{{player.user.get_profile.thumbnail.url}}"{% else %}src="{{STATIC_URL}}img/unknown_thumbnail.png"{% endif %}
|
||||
class="avatar" alt=""
|
||||
title="{% if player.dan_points != None %}Dan P.: {{player.dan_points}}{% else %}Kyu P.: {{player.kyu_points}}{% endif %} - {{player.comment}}"/></a>
|
||||
<h4>{{player.placement}}. - <a href="{% url 'player-ladder-score' player.user %}">{{ player.user }}</a></h4>
|
||||
<strong>{% trans 'Score' %}:</strong> {{player.score}}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if not hanchan.valid %}
|
||||
<p class="grid_12 error"><strong>Ungültig:</strong> {{hanchan.check_validity}}</p>
|
||||
{% elif not hanchan.confirmed %}
|
||||
<p class="grid_12 error">Diese Hanchan wurde nicht anerkannt und wird daher nicht gezählt.</p>
|
||||
{% elif hanchan.comment %}
|
||||
<p class="grid_12">{{ hanchan.comment }}</p>
|
||||
{% endif %}
|
||||
<p class="grid_12 more_link">
|
||||
{% if perms.mahjong_ranking.delete_hanchan %}
|
||||
<a href="{% url 'delete-hanchan' hanchan.pk %}" class="button"><img src="{{STATIC_URL}}icons/table_delete.png" alt="{% trans 'Delete' %}"/> {% trans 'Delete Hanchan' %}</a>
|
||||
{% endif %}
|
||||
{% if perms.mahjong_ranking.change_hanchan %}
|
||||
<a href="{% url 'edit-hanchan' hanchan.pk %}" class="button"><img src="{{STATIC_URL}}icons/table_edit.png" alt="{% trans 'Edit' %}"/> {% trans 'Edit Hanchan' %}</a>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% empty %}
|
||||
<h3 class="grid_12">{% trans 'No Hanchan has been added to this event yet.'%}</h3>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block buttonbar %}
|
||||
{% if perms.mahjong_ranking.add_hanchan %}
|
||||
<a class="button" href="{{event.get_edit_url}}"><img src="{{STATIC_URL}}icons/calendar_edit.png" alt="{% trans 'Add' %}"/> {% trans 'Edit Event' %}</a>
|
||||
<a class="button" href="{% url 'add-hanchan-form' event.id %}"><img src="{{STATIC_URL}}icons/table_add.png" alt="{% trans 'Add' %}"/> {% trans 'Add Hanchan' %}</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
58
src/templates/mahjong_ranking/kyudanranking_list.html
Normal file
58
src/templates/mahjong_ranking/kyudanranking_list.html
Normal file
@@ -0,0 +1,58 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans 'Player List' %}{% endblock %}
|
||||
|
||||
{% block maincontent %}
|
||||
<h2 class="grid_12">{% trans 'Player List' %}</h2>
|
||||
<table class="grid_12">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans 'Avatar' %}</th>
|
||||
<th>
|
||||
<a href="{% url 'kyudanranking-list' order_by='+username' %}?page={{page_obj.number}}"><img src="{{STATIC_URL}}icons/bullet_arrow_up.png" alt="▴"/></a>
|
||||
{% trans 'Nickname' %}
|
||||
<a href="{% url 'kyudanranking-list' order_by='-username' %}?page={{page_obj.number}}"><img src="{{STATIC_URL}}icons/bullet_arrow_down.png" alt="▾"/></a>
|
||||
</th>
|
||||
<th>
|
||||
<a href="{% url 'kyudanranking-list' order_by='+full_name'%}?page={{page_obj.number}}"><img src="{{STATIC_URL}}icons/bullet_arrow_up.png" alt="▴"/></a>
|
||||
{% trans 'Full Name' %}
|
||||
<a href="{% url 'kyudanranking-list' order_by='-full_name' %}?page={{page_obj.number}}"><img src="{{STATIC_URL}}icons/bullet_arrow_down.png" alt="▾"/></a>
|
||||
</th>
|
||||
<th>
|
||||
<a href="{% url 'kyudanranking-list' order_by='+rank' %}?page={{page_obj.number}}"><img src="{{STATIC_URL}}icons/bullet_arrow_up.png" alt="▴"/></a>
|
||||
{% trans 'Rank' %}
|
||||
<a href="{% url 'kyudanranking-list' order_by='-rank' %}?page={{page_obj.number}}"><img src="{{STATIC_URL}}icons/bullet_arrow_down.png" alt="▾"/></a>
|
||||
</th>
|
||||
<th>
|
||||
<a href="{% url 'kyudanranking-list' order_by='+score' %}?page={{page_obj.number}}"><img src="{{STATIC_URL}}icons/bullet_arrow_up.png" alt="▴"/></a>
|
||||
{% trans 'Score' %}
|
||||
<a href="{% url 'kyudanranking-list' order_by='-score' %}?page={{page_obj.number}}"><img src="{{STATIC_URL}}icons/bullet_arrow_down.png" alt="▾"/></a>
|
||||
</th>
|
||||
<th>
|
||||
<a href="{% url 'kyudanranking-list' order_by='+hanchan_count'%}?page={{page_obj.number}}"><img src="{{STATIC_URL}}icons/bullet_arrow_up.png" alt="▴"/></a>
|
||||
{% trans 'Games Total' %}
|
||||
<a href="{% url 'kyudanranking-list' order_by='-hanchan_count'%}?page={{page_obj.number}}"><img src="{{STATIC_URL}}icons/bullet_arrow_down.png" alt="▾"/></a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for ranking in kyudanranking_list %}{% with ranking.user.get_profile as profile %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if profile.thumbnail %}
|
||||
<a href="{{ ranking.get_absolute_url }}"><img src="{{profile.thumbnail.url}}" alt="" /></a>
|
||||
{% else %}
|
||||
<a href="{{ ranking.get_absolute_url }}"><img src="{{STATIC_URL}}img/unknown_thumbnail.png" alt=""/></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><a href="{{ ranking.get_absolute_url }}">{{ ranking.user }}</a></td>
|
||||
<td>{{profile.last_name}} {{profile.first_name}}</td>
|
||||
<td>{% if ranking.dan %} {{ranking.dan}}. Dan {% else %} {{ranking.kyu}} Kyu {% endif %}</td>
|
||||
<td class="right">{% if ranking.dan %} {{ranking.dan_points}} {% else %} {{ranking.kyu_points}} {% endif %}</td>
|
||||
<td class="right">{{ranking.hanchan_count}}</td>
|
||||
</tr>
|
||||
{% endwith %}{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
||||
72
src/templates/mahjong_ranking/ladderranking_list.html
Normal file
72
src/templates/mahjong_ranking/ladderranking_list.html
Normal file
@@ -0,0 +1,72 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n comments%}
|
||||
|
||||
{% block content %}
|
||||
<h2>Ladder - {{season.name}}</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="2">{% trans "Rank" %}</th>
|
||||
<th rowspan="2">{% trans "Avatar" %}</th>
|
||||
<th rowspan="2">{% trans "Nickname" %}</th>
|
||||
<th rowspan="2">{% trans "Name" %}</th>
|
||||
<th colspan="2">{% trans 'Average' %}</th>
|
||||
<th colspan="2">Hanchans</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans 'Placement' %}</th>
|
||||
<th>{% trans "Score" %}</th>
|
||||
<th>{% trans "won" %}</th>
|
||||
<th>{% trans "good" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for player in ladderranking_list %}
|
||||
{% with player.user.get_profile as profile %}
|
||||
<tr>
|
||||
<td class="center">{{player.placement}}.</td>
|
||||
<td class="avatar"><a href="{{ player.get_absolute_url }}?season={{season.id}}">
|
||||
{% if profile.thumbnail %}
|
||||
<img src="{{profile.thumbnail.url}}" alt="" />
|
||||
{% else %}
|
||||
<img src="{{STATIC_URL}}img/unknown_thumbnail.png" alt=""/>
|
||||
{% endif %}
|
||||
</a></td>
|
||||
<td><a href="{{ player.get_absolute_url }}?season={{season.id}}">{{player.user}}</a></td>
|
||||
<td>{{profile.last_name}} {{profile.first_name}}</td>
|
||||
<td class="center">{{player.avg_placement|floatformat:2 }}</td>
|
||||
<td class="right">{{player.avg_score|floatformat:0 }}</td>
|
||||
<td class="center">{{player.won_hanchans}}</td>
|
||||
<td class="center">{{player.good_hanchans}}</td>
|
||||
</tr>
|
||||
{% endwith %}
|
||||
{% empty %}
|
||||
<tr><td colspan="8">Leider hat es noch niemand in das Ranking geschafft.
|
||||
Ein Spieler wird erst ins Ranking genommen wenn er 5 Hanchans absolviert hat.</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% if is_archive %}
|
||||
<h2>{% trans 'Ladder Archive' %}</h2>
|
||||
<ul class="list">
|
||||
{% for season in season_archive %}
|
||||
<li class="season"><a href="{% url 'mahjong-ladder-archive' season.id %}">{{season.name}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<h2>{% trans 'Latest Events' %}</h2>
|
||||
<ul class="list">
|
||||
{% for event in latest_event_list %}
|
||||
<li class="event"><a href="{% url 'event-hanchan-list' event.pk %}">{{event.name}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h2>{% trans 'Latest Hanchans' %}</h2>
|
||||
<ul class="comment_list">
|
||||
{% for hanchan in latest_hanchan_list %}
|
||||
<li class="hanchan"><a href="{% url 'event-hanchan-list' hanchan.event.pk %}">{{hanchan.event.name}}</a> {{hanchan.start|time:'H:i'}}: {{hanchan.player_names}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
10
src/templates/mahjong_ranking/page.html
Normal file
10
src/templates/mahjong_ranking/page.html
Normal file
@@ -0,0 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block additional_nav_elements %}
|
||||
<a href="{% url 'season_ranking-archive' %}" {% if is_archive %}class="active"{% endif %}>{% trans 'Archive' %}</a>
|
||||
{% if perms.events.add_event %}
|
||||
<a href="{% url 'event-form' %}" {% ifequal request.path '/events/add/' %}class="active"{% endifequal %}>{% trans 'Add Event' %}</a>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user