5 Commits
Author SHA1 Message Date
Martin Bley 3322649f05 Code optimizsations 2026-09-19 09:04:26 +02:00
Martin Bley 6b99dd5250 venv is running 2026-09-19 07:58:45 +02:00
Martin Bley d4dbd3d9da Updatet venv 2026-09-19 07:32:04 +02:00
Martin Bley 5b7f847bc6 Models optimiert 2026-09-13 08:51:38 +02:00
Martin Bley 99eaafff00 - getSeason() only for staff members
- new view staff()
- updated bootstrap to version 3.4.1
- updated jquery to 3.6.3
- changed container layout in main view 'matchday'
2023-04-02 21:24:20 +02:00
13 changed files with 735 additions and 498 deletions
+45 -9
View File
@@ -1,10 +1,46 @@
*.pyc # Virtuelle Umgebung
*.swp venv/
bin/ env/
manage.py ENV/
.venv
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Django
*.log
db.sqlite3
media/ media/
old_stuff/ staticfiles/
static/
templates/blogindex.html-media # IDE
tippy/static/ .vscode/
warnings.txt .idea/
*.swp
*.swo
*~
# Umgebungsvariablen
.env
.env.local
+6
View File
@@ -0,0 +1,6 @@
- getSeason() only for staff members
- new view staff()
- updated bootstrap to version 3.4.1
- updated jquery to 3.6.3
- changed container layout in main view 'matchday'
+1 -11
View File
@@ -16,7 +16,6 @@ import os
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
@@ -28,9 +27,7 @@ DEBUG = True
ALLOWED_HOSTS = [] ALLOWED_HOSTS = []
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
@@ -77,10 +74,9 @@ WSGI_APPLICATION = 'TipPy.wsgi.application'
# Database # Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases # https://docs.djangoproject.com/en/4.0/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2', 'ENGINE': 'django.db.backends.postgresql',
'NAME': 'tippy', 'NAME': 'tippy',
'USER': 'tippy', 'USER': 'tippy',
'PASSWORD': 'tippy', 'PASSWORD': 'tippy',
@@ -91,7 +87,6 @@ DATABASES = {
# Password validation # Password validation
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ {
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
@@ -109,13 +104,9 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/ # https://docs.djangoproject.com/en/4.0/topics/i18n/
LANGUAGE_CODE = 'de-de' LANGUAGE_CODE = 'de-de'
TIME_ZONE = 'Europe/Berlin' TIME_ZONE = 'Europe/Berlin'
USE_I18N = True USE_I18N = True
USE_TZ = True USE_TZ = True
# Absolute filesystem path to the directory that will hold user-uploaded files. # Absolute filesystem path to the directory that will hold user-uploaded files.
@@ -149,7 +140,6 @@ STATICFILES_DIRS = [
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Redirect on logout # Redirect on logout
Executable
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "TipPy.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()
+8 -6
View File
@@ -1,6 +1,8 @@
Django~=4.0.6 asgiref==3.12.1
psycopg2~=2.9.3 Django==6.1.1
suds~=1.1.2 pillow==12.3.0
Pillow~=9.2.0 psycopg2==2.9.13
pytz~=2022.1 pytz==2026.3.post1
urllib3~=1.26.10 sqlparse==0.6.0
suds-community==1.2.0
urllib3==2.8.0
+2 -2
View File
@@ -36,7 +36,7 @@
<body> <body>
<!-- jQuery (wird für Bootstrap JavaScript-Plugins benötigt) --> <!-- jQuery (wird für Bootstrap JavaScript-Plugins benötigt) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.3/jquery.min.js"></script>
<!-- Binde alle kompilierten Plugins zusammen ein (wie hier unten) oder such dir einzelne Dateien nach Bedarf aus --> <!-- Binde alle kompilierten Plugins zusammen ein (wie hier unten) oder such dir einzelne Dateien nach Bedarf aus -->
<script src="{% static 'js/bootstrap.min.js'%}"></script> <script src="{% static 'js/bootstrap.min.js'%}"></script>
@@ -53,7 +53,7 @@
<div class="col-md-7"> <div class="col-md-7">
<p> <p>
<div class="pull-left"> <div class="pull-left">
© Martin Bley, 2014-2022 | Dank an © Martin Bley, 2014-2023 | Dank an
<a href="http://openligadb.de">OpenLigaDB</a> für die Bereitstellung der <a href="http://openligadb.de">OpenLigaDB</a> für die Bereitstellung der
Spielergebnisse. Spielergebnisse.
</div> </div>
+6 -11
View File
@@ -12,8 +12,8 @@
{% block body %} {% block body %}
<div id="content-main"> <div id="content-main">
<div class="row"> <div class="row">
<div class="col-md-7"> <div class="col-md-6">
<h2>Deine Tipps</h2> <h3>Deine Tipps</h3>
<nav> <nav>
<ul class="pager"> <ul class="pager">
<li data-toggle="tooltip" title="{% trans 'previous' %}" class="previous"><a <li data-toggle="tooltip" title="{% trans 'previous' %}" class="previous"><a
@@ -67,12 +67,9 @@
<span class="glyphicon glyphicon-saved" aria-hidden="true"></span> Tipps abgeben <span class="glyphicon glyphicon-saved" aria-hidden="true"></span> Tipps abgeben
</button> </button>
</form> </form>
</div>
</div>
<div class="row">
{% for mandant, list in mandant_dict.items %} {% for mandant, list in mandant_dict.items %}
<div class="col-md-7"> <h3>Mitspieler {{ mandant }}</h3>
<h4>Mitspieler {{ mandant }}</h4>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover table-condensed"> <table class="table table-hover table-condensed">
<tr> <tr>
@@ -105,12 +102,10 @@
</tr> </tr>
</table> </table>
</div> </div>
</div>
{% endfor %} {% endfor %}
</div> </div>
<div class="row"> <div class="col-md-6">
<div class="col-md-7"> <h3>Chat</h3>
<h2>Chat</h2>
{% block chat %} {% block chat %}
{% include "chat.xhtml" with posts=posts %} {% include "chat.xhtml" with posts=posts %}
{% endblock %} {% endblock %}
+3
View File
@@ -16,6 +16,9 @@
<li><a href="/">Tipps</a></li> <li><a href="/">Tipps</a></li>
<li><a href="/charts/{{ ls }}/{{ season }}{% if pos %}/{{ pos }}{% endif %}">Bestenliste</a></li> <li><a href="/charts/{{ ls }}/{{ season }}{% if pos %}/{{ pos }}{% endif %}">Bestenliste</a></li>
<li><a href="/blog/1">Blog</a></li> <li><a href="/blog/1">Blog</a></li>
{% if request.user.is_active and request.user.is_staff %}
<li><a href="/staff">Staff</a></li>
{% endif %}
</ul> </ul>
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
{% if has_refresh %} {% if has_refresh %}
+34
View File
@@ -0,0 +1,34 @@
{% extends "base.html" %}
{% load i18n static %}
{% load humanize %}
{% block head %}
<script type="text/javascript" src="{% static 'javascript/tippy.js' %}"></script>
{% endblock %}
{% block nav %}
{% include "nav.html" with ls=ls season=season %}
{% endblock %}
{% block body %}
<div id="content-main">
<div class="row">
<div class="col-md-12">
<h3>Wettbewerbe</h3>
{% for comp in competitions %}
{{ comp }}<br/>
{% endfor %}
<h3></h3>
</div>
</div>
{% if debug %}
<div class="row">
<div class="col-md-12">
<h2 class="form-signin-heading">{% trans 'Debug' %}</h2>
{% for line in debug %}
<p>{{ line }}</p>
{% endfor %}
</div>
</div>
{% endif %}
</div>
{% endblock %}
+27 -7
View File
@@ -18,8 +18,8 @@ You should have received a copy of the GNU General Public License
along with TipPy. If not, see <http://www.gnu.org/licenses/>. along with TipPy. If not, see <http://www.gnu.org/licenses/>.
""" """
from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import models
class UserProfile(models.Model): class UserProfile(models.Model):
user = models.OneToOneField( user = models.OneToOneField(
@@ -31,13 +31,18 @@ class UserProfile(models.Model):
def __unicode__(self): def __unicode__(self):
return self.user.username return self.user.username
class Competition(models.Model): class Competition(models.Model):
leagueShortcut = models.CharField(max_length=32) leagueShortcut = models.CharField(max_length=32)
leagueName = models.CharField(max_length=128) leagueName = models.CharField(max_length=128)
season = models.CharField(max_length=4) season = models.CharField(max_length=4)
class Meta:
unique_together = ("leagueShortcut", "season")
def __unicode__(self): def __unicode__(self):
return(self.leagueShortcut) return (self.leagueShortcut)
class Team(models.Model): class Team(models.Model):
teamID = models.IntegerField(unique=True, primary_key=True) teamID = models.IntegerField(unique=True, primary_key=True)
@@ -55,11 +60,17 @@ class Team(models.Model):
class Match(models.Model): class Match(models.Model):
matchID = models.IntegerField(unique=True,primary_key=True) matchID = models.IntegerField(unique=True, primary_key=True)
matchDateTime = models.DateTimeField() matchDateTime = models.DateTimeField()
group = models.IntegerField() group = models.IntegerField()
matchday = models.IntegerField() matchday = models.IntegerField()
matchday_name = models.CharField(max_length=128) matchday_name = models.CharField(max_length=128)
competition = models.ForeignKey(
Competition,
on_delete=models.CASCADE
)
idTeam1 = models.ForeignKey( idTeam1 = models.ForeignKey(
Team, related_name='+', Team, related_name='+',
on_delete=models.CASCADE on_delete=models.CASCADE
@@ -68,15 +79,15 @@ class Match(models.Model):
Team, related_name='+', Team, related_name='+',
on_delete=models.CASCADE on_delete=models.CASCADE
) )
pointsTeam1 = models.SmallIntegerField() pointsTeam1 = models.SmallIntegerField()
pointsTeam2 = models.SmallIntegerField() pointsTeam2 = models.SmallIntegerField()
finished = models.BooleanField() finished = models.BooleanField()
season = models.CharField(max_length=4)
leagueShortcut = models.CharField(max_length=12)
def __unicode__(self): def __unicode__(self):
return (str(self.matchID)) return (str(self.matchID))
class Tipp(models.Model): class Tipp(models.Model):
matchID = models.ForeignKey( matchID = models.ForeignKey(
Match, Match,
@@ -95,12 +106,15 @@ class Tipp(models.Model):
class Meta: class Meta:
unique_together = ("matchID", "tipperID") unique_together = ("matchID", "tipperID")
class Mandant(models.Model): class Mandant(models.Model):
name = models.CharField(max_length=32, unique=True) name = models.CharField(max_length=32, unique=True)
description = models.CharField(max_length=255) description = models.CharField(max_length=255)
def __unicode__(self): def __unicode__(self):
return self.name return self.name
class Score(models.Model): class Score(models.Model):
client = models.ForeignKey( client = models.ForeignKey(
Mandant, Mandant,
@@ -110,10 +124,12 @@ class Score(models.Model):
exact = models.PositiveSmallIntegerField() exact = models.PositiveSmallIntegerField()
diff = models.PositiveSmallIntegerField() diff = models.PositiveSmallIntegerField()
tendency = models.PositiveSmallIntegerField() tendency = models.PositiveSmallIntegerField()
def __unicode__(self): def __unicode__(self):
return str(self.client) return str(self.client)
class chart(models.Model):
class Chart(models.Model):
team = models.ForeignKey( team = models.ForeignKey(
Team, Team,
on_delete=models.CASCADE on_delete=models.CASCADE
@@ -129,6 +145,7 @@ class chart(models.Model):
class Meta: class Meta:
unique_together = ("team", "competition") unique_together = ("team", "competition")
class RelUserMandant(models.Model): class RelUserMandant(models.Model):
user = models.ForeignKey( user = models.ForeignKey(
User, User,
@@ -141,9 +158,11 @@ class RelUserMandant(models.Model):
class Meta: class Meta:
unique_together = ("user", "mandant") unique_together = ("user", "mandant")
def __unicode__(self): def __unicode__(self):
return (str(self.user) + " -> " + str(self.mandant)) return (str(self.user) + " -> " + str(self.mandant))
class Post(models.Model): class Post(models.Model):
content = models.TextField() content = models.TextField()
published = models.BooleanField(default=True) published = models.BooleanField(default=True)
@@ -159,6 +178,7 @@ class Post(models.Model):
def __unicode__(self): def __unicode__(self):
return u'%s' % self.created return u'%s' % self.created
class RelPostMandant(models.Model): class RelPostMandant(models.Model):
post = models.ForeignKey( post = models.ForeignKey(
Post, Post,
@@ -171,6 +191,6 @@ class RelPostMandant(models.Model):
class Meta: class Meta:
unique_together = ("post", "mandant") unique_together = ("post", "mandant")
def __unicode__(self): def __unicode__(self):
return (str(self.post) + " -> " + str(self.mandant)) return (str(self.post) + " -> " + str(self.mandant))
+22 -33
View File
@@ -18,13 +18,15 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with TipPy. If not, see <http://www.gnu.org/licenses/>. # along with TipPy. If not, see <http://www.gnu.org/licenses/>.
# #
import sys
import locale import locale
from suds.client import Client
import urllib.request
import urllib.error import urllib.error
import urllib.parse import urllib.parse
import psycopg2 import urllib.request
from zeep import Client
from zeep.transports import Transport
from requests import Session
class OpenLiga(object): class OpenLiga(object):
version = "0.1" version = "0.1"
@@ -40,57 +42,44 @@ class OpenLiga(object):
self.debug = debug self.debug = debug
# get a SUDS client object # get a SOAP client object
if self.proxyurl is None: wsdl_url = 'http://www.openligadb.de/Webservices/Sportsdata.asmx?WSDL'
try: try:
self.client = Client( if proxyurl is None:
'http://www.openligadb.de/' self.client = Client(wsdl=wsdl_url)
+ 'Webservices/Sportsdata.asmx?WSDL')
except (urllib.error.URLError):
self.error += "Connect to webservice failed."
else: else:
try: session = Session()
t = suds.transport.http.HttpTransport() session.proxies = {'http': proxyurl, 'https': proxyurl}
proxy = urllib.request.ProxyHandler({'http':proxyurl}) transport = Transport(session=session)
opener = urllib.request.build_opener(proxy) self.client = Client(wsdl=wsdl_url, transport=transport)
t.urlopener = opener except (urllib.error.URLError, Exception) as e:
self.client = suds.client.Client( self.error += f"Connect to webservice failed: {str(e)}\n"
'http://www.openligadb.de/Webservices/'
+ 'Sportsdata.asmx?WSDL', transport=t)
except urllib.error.URLError as e:
self.error += "Connect to webservice failed " \
+ "(via proxy " + proxyurl + "): " + str(e) + "\n"
def getSeason(self, season, league='bl1'): def getSeason(self, season, league='bl1'):
""" Get the whole season. """ Get the whole season.
Args: season and league shortcut (optional) Args: season and league shortcut (optional)
""" """
return(self.client.service.GetMatchdataByLeagueSaison( return self.client.service.GetMatchdataByLeagueSaison(
leagueShortcut=league,leagueSaison=season) leagueShortcut=league, leagueSaison=season)
)
def getMatchday(self, season, matchdaynumber, league='bl1'): def getMatchday(self, season, matchdaynumber, league='bl1'):
""" Get matchday. """ Get matchday.
Args: season, matchdaynumber and league shortcut (optional) Args: season, matchdaynumber and league shortcut (optional)
""" """
return(self.client.service.GetMatchdataByGroupLeagueSaison( return self.client.service.GetMatchdataByGroupLeagueSaison(
groupOrderID=matchdaynumber, groupOrderID=matchdaynumber,
leagueShortcut=league, leagueShortcut=league,
leagueSaison=season) leagueSaison=season)
)
def getTeams(self, season, league='bl1'): def getTeams(self, season, league='bl1'):
return(self.client.service.GetTeamsByLeagueSaison( return self.client.service.GetTeamsByLeagueSaison(
leagueShortcut=league, leagueShortcut=league,
leagueSaison=season) leagueSaison=season)
)
def getCurrentGroup(self, league): def getCurrentGroup(self, league):
return(self.client.service.GetCurrentGroup( return self.client.service.GetCurrentGroup(
leagueShortcut=league) leagueShortcut=league)
)
+1
View File
@@ -22,6 +22,7 @@ urlpatterns = [
re_path(r'^blog/newpost$', views.newBlogpost), re_path(r'^blog/newpost$', views.newBlogpost),
re_path(r'^blog/(?P<page>\d)$', views.blogindex), re_path(r'^blog/(?P<page>\d)$', views.blogindex),
re_path(r'^about$', views.about), re_path(r'^about$', views.about),
re_path(r'^staff', views.staff),
re_path(r'^accounts/login/', auth_views.LoginView.as_view()), re_path(r'^accounts/login/', auth_views.LoginView.as_view()),
re_path(r'^accounts/logout/', auth_views.LogoutView.as_view()) re_path(r'^accounts/logout/', auth_views.LogoutView.as_view())
] ]
+503 -365
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Copyright (c) 2015-2016 by Martin Bley (martin@mb-oss.de) Copyright (c) 2015-2016 by Martin Bley (dev@mb-oss.de)
This file is part of TipPy. This file is part of TipPy.
@@ -18,466 +18,604 @@ You should have received a copy of the GNU General Public License
along with TipPy. If not, see <http://www.gnu.org/licenses/>. along with TipPy. If not, see <http://www.gnu.org/licenses/>.
""" """
import django.shortcuts
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator, EmptyPage, \
PageNotAnInteger
from django.http import HttpResponseRedirect
from django.utils import timezone
from django.db.models import Sum
from django.conf import settings
from os.path import join as pjoin
from tipp.models import *
from tipp.forms import *
from tipp.openliga import *
from datetime import datetime
from PIL import Image as PImage
import pytz
import os import os
from os.path import join as pjoin
timezoneLocal = pytz.timezone('Europe/Berlin') import logging
import pytz
from PIL import Image as PImage
from django.conf import settings
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from django.core.files.storage import default_storage
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.db.models import Sum
from django.http import HttpResponseRedirect
from django.shortcuts import render, redirect, get_object_or_404
from django.utils import timezone
from django.views.decorators.http import require_http_methods
from tipp.forms import *
from tipp.models import *
from tipp.openliga import *
TIMEZONE_LOCAL = pytz.timezone('Europe/Berlin')
THUMBNAIL_SIZE = (50, 50)
logger = logging.getLogger(__name__)
def about(request): def about(request):
return django.shortcuts.render(request, 'about.html') """Render about page."""
return render(request, 'about.html')
@login_required @login_required
def home(request): def home(request):
"""Redirect to current matchday."""
ls = get_current_ls() ls = get_current_ls()
season = get_current_season() season = get_current_season()
md = get_current_md(ls) md = get_current_md(ls)
return django.shortcuts.redirect("matchday", return redirect(
'matchday',
ls=ls, ls=ls,
season=season, season=season,
matchday=str(md).zfill(2) matchday=str(md).zfill(2)
) )
def _create_thumbnail(image_path):
"""Create a 50x50 thumbnail from the original image."""
try:
im = PImage.open(image_path)
im.thumbnail(THUMBNAIL_SIZE, PImage.Resampling.LANCZOS)
im.save(image_path)
except Exception as e:
logger.error(f"Failed to create thumbnail: {str(e)}")
raise
def _delete_avatar(image_path):
"""Delete avatar image file safely."""
if os.path.isfile(image_path):
try:
os.remove(image_path)
except OSError as e:
logger.error(f"Failed to delete avatar: {str(e)}")
@login_required @login_required
@require_http_methods(["GET", "POST"])
def profile(request, pk): def profile(request, pk):
user = User.objects.get(id=pk) """Display and edit user profile."""
user = get_object_or_404(User, id=pk)
profile, created = UserProfile.objects.get_or_create(user=user) profile, created = UserProfile.objects.get_or_create(user=user)
if profile.avatar:
img = profile.avatar.name
else:
img = None
debug = []
debug.append('avatar: ' + str(img))
# If it's a HTTP POST, we're interested in processing form data.
if request.method == 'POST': if request.method == 'POST':
return _handle_profile_post(request, user, profile)
else:
return _handle_profile_get(request, user, profile)
def _handle_profile_post(request, user, profile):
"""Handle POST request for profile update."""
user_form = UserForm(data=request.POST, instance=user) user_form = UserForm(data=request.POST, instance=user)
profile_form = UserProfileForm( profile_form = UserProfileForm(data=request.POST, instance=profile)
data=request.POST, instance=profile
)
if user_form.is_valid() and profile_form.is_valid(): if user_form.is_valid() and profile_form.is_valid():
user = user_form.save() user = user_form.save()
profile = profile_form.save(commit=False) profile = profile_form.save(commit=False)
profile.user = user profile.user = user
# Did the user provide a profile picture? # Handle avatar upload
if 'avatar' in request.FILES: if 'avatar' in request.FILES:
profile.avatar = request.FILES['avatar'] profile.avatar = request.FILES['avatar']
# Now we save the UserProfile model instance.
profile.save() profile.save()
# create thumbnail image_path = os.path.join(settings.MEDIA_ROOT, profile.avatar.name)
imfn = pjoin(settings.MEDIA_ROOT, profile.avatar.name) try:
im = PImage.open(imfn) _create_thumbnail(image_path)
im.thumbnail((50, 50), PImage.ANTIALIAS) except Exception as e:
im.save(settings.MEDIA_ROOT + profile.avatar.name) logger.error(f"Thumbnail creation failed: {str(e)}")
if 'avatar-clear' in request.POST:
if 'on' in request.POST['avatar-clear']: # Handle avatar deletion
image = pjoin(settings.MEDIA_ROOT, img) elif 'avatar-clear' in request.POST and request.POST['avatar-clear'] == 'on':
debug.append('Is the image ' + str(image) if profile.avatar:
+ ' a regular file? ' old_image_path = os.path.join(settings.MEDIA_ROOT, profile.avatar.name)
+ str(os.path.isfile(image)) _delete_avatar(old_image_path)
) profile.avatar = None
if os.path.isfile(image):
os.remove(image)
profile.save() profile.save()
return redirect('profile', pk=user.id)
for item in request.POST:
debug.append(str(item) + " -> "
+ str(request.POST[item])
)
# Invalid form or forms - mistakes or something else?
# Print problems to the terminal.
# They'll also be shown to the user.
else: else:
print( logger.warning(f"Form validation failed for user {user.id}: "
user_form.errors, f"User errors: {user_form.errors}, "
profile_form.errors f"Profile errors: {profile_form.errors}")
)
return django.shortcuts.redirect('/accounts/profile/' + str(user.id)) return _render_profile_form(request, user_form, profile_form, profile)
# Not a HTTP POST, so we render our form using two ModelForm
# instances. These forms will be blank, ready for user input. def _handle_profile_get(request, user, profile):
else: """Handle GET request for profile display."""
user_form = UserForm( user_form = UserForm(instance=user)
initial={'last_name': user.last_name,
'first_name': user.first_name,
'email': user.email},
instance=user
)
profile_form = UserProfileForm(instance=profile) profile_form = UserProfileForm(instance=profile)
return django.shortcuts.render(request, 'registration/profile.html', { return _render_profile_form(request, user_form, profile_form, profile)
def _render_profile_form(request, user_form, profile_form, profile):
"""Render profile form template."""
context = {
'user_form': user_form, 'user_form': user_form,
'profile_form': profile_form, 'profile_form': profile_form,
'debug': debug, 'avatar': profile.avatar.name if profile.avatar else None,
'img': img} }
) return render(request, 'registration/profile.html', context)
@staff_member_required
def staff(request):
"""Display available competitions for current season."""
season = get_current_season()
competitions = Competition.objects.filter(
season=season
).order_by('leagueName')
return render(request, 'staff.html', {'competitions': competitions})
@login_required @staff_member_required
def getSeason(request, ls, season): def getSeason(request, ls, season):
"""Fetch and import teams and matches for a season from OpenLiga."""
ol = OpenLiga() ol = OpenLiga()
teams = ol.getTeams(str(season), ls) teams_data = ol.getTeams(str(season), ls)
season = ol.getSeason(str(season), ls) season_data = ol.getSeason(str(season), ls)
return_vals = [] # Bulk import teams
for team in teams[0]: _import_teams(teams_data[0])
t = Team(
teamID=team['teamID'],
name=team['teamName'],
iconURL=team['teamIconURL']
)
""" # Bulk import matches
check if object exits and do insert if not _import_season_matches(season_data[0], ls)
"""
try:
to = Team.objects.get(teamID=int(t.teamID))
if to.iconURL is None:
t.save(update_fields=['iconURL'])
except Team.DoesNotExist:
t.save()
for match in season[0]: return redirect(
m = Match(matchID=match['matchID'], 'matchday',
matchDateTime=match['matchDateTime'],
group=int(match['groupID']),
matchday=int(match['groupOrderID']),
matchday_name=match['groupName'],
idTeam1=Team.objects.get(teamID=int(match['idTeam1'])),
idTeam2=Team.objects.get(teamID=int(match['idTeam2'])),
pointsTeam1=match['pointsTeam1'],
pointsTeam2=match['pointsTeam2'],
finished=bool(match['matchIsFinished']),
season=match['leagueSaison'],
leagueShortcut=match['leagueShortcut']
)
m.save()
return django.shortcuts.redirect("matchday",
ls=ls, ls=ls,
season=get_current_season(), season=get_current_season(),
matchday=str(get_current_md(ls)).zfill(2) matchday=str(get_current_md(ls)).zfill(2)
) )
def _import_teams(teams_data):
"""Import teams, update existing ones with icon URL if missing."""
teams_to_create = []
teams_to_update = []
for team_data in teams_data:
team_id = int(team_data['teamID'])
try:
existing_team = Team.objects.get(teamID=team_id)
if existing_team.iconURL is None:
existing_team.iconURL = team_data['teamIconURL']
teams_to_update.append(existing_team)
except Team.DoesNotExist:
teams_to_create.append(
Team(
teamID=team_id,
name=team_data['teamName'],
iconURL=team_data['teamIconURL']
)
)
if teams_to_create:
Team.objects.bulk_create(teams_to_create, ignore_conflicts=True)
if teams_to_update:
Team.objects.bulk_update(teams_to_update, ['iconURL'], batch_size=100)
def _import_season_matches(season_data, ls):
"""Import all matches from season data."""
matches = []
for match_data in season_data:
try:
team1 = Team.objects.get(teamID=int(match_data['idTeam1']))
team2 = Team.objects.get(teamID=int(match_data['idTeam2']))
except Team.DoesNotExist:
logger = logging.getLogger(__name__)
logger.warning(f"Team not found for match {match_data['matchID']}")
continue
matches.append(
Match(
matchID=match_data['matchID'],
matchDateTime=match_data['matchDateTime'],
group=int(match_data['groupID']),
matchday=int(match_data['groupOrderID']),
matchday_name=match_data['groupName'],
idTeam1=team1,
idTeam2=team2,
pointsTeam1=match_data['pointsTeam1'],
pointsTeam2=match_data['pointsTeam2'],
finished=bool(match_data['matchIsFinished']),
season=match_data['leagueSaison'],
leagueShortcut=match_data['leagueShortcut']
)
)
if matches:
Match.objects.bulk_create(matches, ignore_conflicts=True)
@login_required @login_required
def update(request, ls, season, cur_md): def update(request, ls, season, cur_md):
""" """Update match results and calculate user scores for current matchday."""
To do:
- update table tipp_competition
"""
ol = OpenLiga() ol = OpenLiga()
md_matches = ol.getMatchday(str(season), int(cur_md), ls) md_matches = ol.getMatchday(str(season), int(cur_md), ls)
# Achtung: DateTimeField prüfen # Update match results
for match in md_matches[0]: _update_match_results(md_matches[0])
# Endergebnis holen
matchResults = match['matchResults']
try:
for result in matchResults[0]:
if result['resultName'] == "Halbzeit":
pointsTeam1 = int(result['pointsTeam1'])
pointsTeam2 = int(result['pointsTeam2'])
elif result['resultName'] == "Endergebnis":
pointsTeam1 = int(result['pointsTeam1'])
pointsTeam2 = int(result['pointsTeam2'])
break
elif str(result['resultName']) == u"nach Verlängerung":
pointsTeam1 = int(result['pointsTeam1'])
pointsTeam2 = int(result['pointsTeam2'])
else:
pointsTeam1 = -1
pointsTeam2 = -1
except: # Calculate scores for all tips
pointsTeam1 = -1 _calculate_matchday_scores(request.user, ls, season, cur_md)
pointsTeam2 = -1
m = Match(matchID=match['matchID'], return redirect('matchday', ls=ls, season=season, matchday=cur_md)
matchDateTime=match['matchDateTime'],
pointsTeam1=pointsTeam1,
pointsTeam2=pointsTeam2, def _update_match_results(matches_data):
finished=bool(match['matchIsFinished']), """Update match results from OpenLiga data."""
matches_to_update = []
for match_data in matches_data:
points_team1, points_team2 = _extract_match_result(match_data['matchResults'])
matches_to_update.append(
Match(
matchID=match_data['matchID'],
matchDateTime=match_data['matchDateTime'],
pointsTeam1=points_team1,
pointsTeam2=points_team2,
finished=bool(match_data['matchIsFinished']),
)
) )
m.save(update_fields=['matchDateTime', 'pointsTeam1', 'pointsTeam2', 'finished'])
pointsTeam1 = -1
pointsTeam2 = -1
# evaluate scores if matches_to_update:
""" get mandants current user is related """ Match.objects.bulk_update(
mandants = RelUserMandant.objects.filter(user=request.user). \ matches_to_update,
values_list('mandant', flat=True) ['matchDateTime', 'pointsTeam1', 'pointsTeam2', 'finished'],
batch_size=100
)
# scores = Score.objects.get(client=Mandant.objects.get(name='huse'))
scores = Score.objects.get(client=mandants[0]) def _extract_match_result(match_results):
"""Extract final match result from match results data.
Returns:
tuple: (pointsTeam1, pointsTeam2), defaults to (-1, -1) if not found.
"""
if not match_results or not match_results[0]:
return -1, -1
try:
for result in match_results[0]:
result_name = result['resultName']
# Prefer final result, but accept extra time if available
if result_name in ("Endergebnis", "nach Verlängerung"):
return int(result['pointsTeam1']), int(result['pointsTeam2'])
# Fallback: half-time result if final not found
for result in match_results[0]:
if result['resultName'] == "Halbzeit":
return int(result['pointsTeam1']), int(result['pointsTeam2'])
except (KeyError, ValueError, TypeError) as e:
logger = logging.getLogger(__name__)
logger.warning(f"Error extracting match result: {str(e)}")
return -1, -1
def _calculate_matchday_scores(user, ls, season, cur_md):
"""Calculate scores for all user tips on finished matches."""
try:
mandant = RelUserMandant.objects.filter(user=user).values_list(
'mandant', flat=True
).first()
if not mandant:
logger = logging.getLogger(__name__)
logger.warning(f"No mandant found for user {user.id}")
return
scores = Score.objects.get(client=mandant)
except Score.DoesNotExist:
logger = logging.getLogger(__name__)
logger.warning(f"No score configuration found for mandant {mandant}")
return
finished_matches = Match.objects.filter( finished_matches = Match.objects.filter(
finished=True, finished=True,
leagueShortcut=ls, leagueShortcut=ls,
season=season, season=season,
matchday=cur_md matchday=cur_md
) ).prefetch_related('tipp_set')
tipps_to_update = []
for match in finished_matches: for match in finished_matches:
tipps = Tipp.objects.filter(matchID=match.matchID) for tipp in match.tipp_set.all():
for tipp in tipps: score = _calculate_tipp_score(tipp, match, scores)
score = 0 tipp.score = score
tipps_to_update.append(tipp)
if tipps_to_update:
Tipp.objects.bulk_update(tipps_to_update, ['score'], batch_size=100)
def _calculate_tipp_score(tipp, match, scores):
"""Calculate score for a single tip based on match result.
Returns:
int: Points earned for this tip.
"""
# Exact result
if tipp.pointsTeam1 == match.pointsTeam1 and tipp.pointsTeam2 == match.pointsTeam2:
goals_total = match.pointsTeam1 + match.pointsTeam2 goals_total = match.pointsTeam1 + match.pointsTeam2
return scores.exact_high if goals_total >= 5 else scores.exact
# Tipp is acurate # Winner and difference
if (tipp.pointsTeam1 == match.pointsTeam1 winner_real = _get_winner(match.pointsTeam1, match.pointsTeam2)
and tipp.pointsTeam2 == match.pointsTeam2): winner_tipp = _get_winner(tipp.pointsTeam1, tipp.pointsTeam2)
if goals_total >= 5:
score = scores.exact_high if winner_tipp == winner_real:
diff_real = match.pointsTeam1 - match.pointsTeam2
diff_tipp = tipp.pointsTeam1 - tipp.pointsTeam2
if diff_tipp == diff_real:
return scores.diff
else: else:
score = scores.exact return scores.tendency
tipp.score = score return 0
tipp.save()
continue
# who won?
winnerReal = None
if match.pointsTeam1 > match.pointsTeam2:
winnerReal = 1
elif match.pointsTeam2 > match.pointsTeam1:
winnerReal = 2
elif match.pointsTeam1 == match.pointsTeam2:
winnerReal = 0
winnerTipp = None def _get_winner(points_team1, points_team2):
if tipp.pointsTeam1 > tipp.pointsTeam2: """Determine match winner.
winnerTipp = 1
elif tipp.pointsTeam2 > tipp.pointsTeam1:
winnerTipp = 2
elif tipp.pointsTeam1 == tipp.pointsTeam2:
winnerTipp = 0
diffReal = match.pointsTeam1 - match.pointsTeam2
diffTipp = tipp.pointsTeam1 - tipp.pointsTeam2
if winnerTipp == winnerReal:
if diffTipp == diffReal:
score = scores.diff
else:
score = scores.tendency
tipp.score = score
tipp.save()
continue
tipp.score = score
tipp.save()
continue
return django.shortcuts.redirect("matchday",
ls=ls,
season=season,
matchday=cur_md
)
Returns:
int: 1 for team1 win, 2 for team2 win, 0 for draw, -1 for invalid.
"""
if points_team1 > points_team2:
return 1
elif points_team2 > points_team1:
return 2
elif points_team1 == points_team2:
return 0
return -1
@login_required @login_required
@require_http_methods(["GET", "POST"])
def matchday(request, ls, season, matchday, template_name='md.html'): def matchday(request, ls, season, matchday, template_name='md.html'):
debug = '' """Display matchday with user tips, match results, and mandant scores."""
debug += "Debugging: " # Process form submissions first
has_refresh = True _process_matchday_tips(request, ls, season, matchday)
""" get matches of the matchday """ user = request.user
md_matches = Match.objects.filter(leagueShortcut=ls, season=season, matches = _build_matchday_matches(request, user, ls, season, matchday)
matchday=int(matchday)).order_by('matchDateTime') mandant_dict = _build_mandant_scores(user, matches)
posts = _get_mandant_posts(user)
""" get the current user """ cur_group = matches[0]['md_name'] if matches else "N/A"
user = User.objects.get(username=request.user.username)
""" get mandants current user is related """ return render(request, template_name, {
mandants = RelUserMandant.objects.filter(user=request.user). \
values_list('mandant', flat=True)
""" get mates """
mandant_dict = {}
for m in mandants:
mandant = Mandant.objects.get(id=m)
mandant_dict[mandant.description] = []
rs = RelUserMandant.objects.filter(mandant=m)
for r in rs:
mandant_dict[mandant.description].append({
'mate': r.user,
'tipps': [],
'sum_score': 0
})
matches = []
for match in md_matches:
"""
create a form for each tipp
"""
form = TippForm(request.POST or None, prefix=str(match.matchID))
"""
check if Tipp already exists
"""
try:
tipp = Tipp.objects.get(tipperID=user.id,
matchID=match.matchID)
except ObjectDoesNotExist:
tipp = None
"""
if request is a POST, process form data, else just show form
"""
if form.is_valid():
data = form.cleaned_data
if tipp is not None:
""" Tipp exists an needs an UPDATE"""
tipp = Tipp(id=tipp.pk,
matchID=match,
tipperID=user,
pointsTeam1=data['tippTeam1'],
pointsTeam2=data['tippTeam2']
)
tipp.save()
else:
""" do INSERT """
tipp = Tipp(matchID=match,
tipperID=user,
pointsTeam1=data['tippTeam1'],
pointsTeam2=data['tippTeam2'])
tipp.save()
match_started = timezone.now() >= match.matchDateTime
if tipp is not None:
f = TippForm(prefix=str(match.matchID),
initial={'tippTeam1': tipp.pointsTeam1,
'tippTeam2': tipp.pointsTeam2}
)
else:
f = TippForm(prefix=str(match.matchID))
item = {}
item['matchDateTime'] = (match.matchDateTime.
astimezone(timezoneLocal).strftime('%a %d.%m, %H:%M'))
item['iconTeam1'] = str(match.idTeam1.icon)
item['iconURLTeam1'] = str(match.idTeam1.iconURL)
item['nameTeam1'] = str(match.idTeam1.name)
item['abbrTeam1'] = str(match.idTeam1.abbr)
item['iconTeam2'] = str(match.idTeam2.icon)
item['iconURLTeam2'] = str(match.idTeam2.iconURL)
item['nameTeam2'] = str(match.idTeam2.name)
item['abbrTeam2'] = str(match.idTeam2.abbr)
item['started'] = match_started
item['finished'] = match.finished
item['md_name'] = match.matchday_name
if (match.pointsTeam1 == -1 or match.pointsTeam2 == -1):
item['matchResult'] = "-:-"
else:
item['matchResult'] = str(match.pointsTeam1) + ":" \
+ str(match.pointsTeam2)
item['tippFormTeam1'] = f['tippTeam1']
item['tippFormTeam2'] = f['tippTeam2']
if match_started is True:
f.fields['tippTeam1'].widget.attrs['disabled'] = True
f.fields['tippTeam2'].widget.attrs['disabled'] = True
""" get tipps from mates """
for mandant in mandant_dict:
for mate in mandant_dict[mandant]:
try:
matetipp = Tipp.objects.get(tipperID=mate['mate'],
matchID=match.matchID)
if match_started is True:
if matetipp.score is None:
score = u'\u231B'
else:
score = matetipp.score
mate['tipps'].append(str(matetipp.pointsTeam1)
+ ":" + str(matetipp.pointsTeam2)
+ "(" + str(score) + ")")
if match.finished is True:
mate['sum_score'] += matetipp.score
else:
mate['tipps'].append(u'\u2714')
except Exception as e:
mate['tipps'].append(u'\u2717')
""" sort items in mandant_dict by score """
mandant_dict[mandant] = sorted(mandant_dict[mandant],
key=lambda k: k['sum_score'], reverse=True)
matches.append(item)
# get the newest blogposts
pm = RelPostMandant.objects.filter(mandant__in=mandants). \
values_list('post', flat=True)
posts = []
for post in Post.objects.filter(id__in=pm, published=True)[:5]:
try:
avatar = UserProfile.objects.get(user_id=post.author_id). \
avatar.name
except:
avatar = None
posts.append((post, avatar))
try:
cur_group = matches[0]['md_name']
except:
cur_group = "N/A"
debug += str(mandant_dict)
return django.shortcuts.render(request, 'md.html', {
'debug': debug,
# 'mds_season': mds_in_season,
'cur_md': matchday, 'cur_md': matchday,
'md_name': cur_group, 'md_name': cur_group,
'matches': matches, 'matches': matches,
'ls': ls, 'ls': ls,
'season': season, 'season': season,
'username': user, 'username': user.username,
'mandant_dict': mandant_dict, 'mandant_dict': mandant_dict,
# 'tipp_mates': sorted(tipp_mates, key=lambda k: k['sum_score'],
# reverse=True),
'posts': posts, 'posts': posts,
'has_refresh': has_refresh 'has_refresh': True
}) })
def _process_matchday_tips(request, ls, season, matchday):
"""Process and save tip form submissions."""
if request.method != 'POST':
return
md_matches = Match.objects.filter(
leagueShortcut=ls,
season=season,
matchday=int(matchday)
)
for match in md_matches:
form = TippForm(request.POST, prefix=str(match.matchID))
if form.is_valid():
data = form.cleaned_data
Tipp.objects.update_or_create(
tipperID=request.user,
matchID=match,
defaults={
'pointsTeam1': data['tippTeam1'],
'pointsTeam2': data['tippTeam2']
}
)
def _build_matchday_matches(request, user, ls, season, matchday):
"""Build match data with forms and results for template."""
md_matches = Match.objects.filter(
leagueShortcut=ls,
season=season,
matchday=int(matchday)
).select_related('idTeam1', 'idTeam2').order_by('matchDateTime')
# Fetch all user tips in one query
user_tipps = {
tipp.matchID_id: tipp
for tipp in Tipp.objects.filter(tipperID=user, matchID__in=md_matches)
}
matches = []
for match in md_matches:
match_started = timezone.now() >= match.matchDateTime
tipp = user_tipps.get(match.id)
# Build form with initial data
initial = {}
if tipp:
initial = {
'tippTeam1': tipp.pointsTeam1,
'tippTeam2': tipp.pointsTeam2
}
form = TippForm(prefix=str(match.matchID), initial=initial)
if match_started:
form.fields['tippTeam1'].widget.attrs['disabled'] = True
form.fields['tippTeam2'].widget.attrs['disabled'] = True
match_result = _format_match_result(match.pointsTeam1, match.pointsTeam2)
item = {
'matchDateTime': match.matchDateTime.astimezone(TIMEZONE_LOCAL).strftime(
'%a %d.%m, %H:%M'
),
'iconTeam1': str(match.idTeam1.icon),
'iconURLTeam1': str(match.idTeam1.iconURL),
'nameTeam1': str(match.idTeam1.name),
'abbrTeam1': str(match.idTeam1.abbr),
'iconTeam2': str(match.idTeam2.icon),
'iconURLTeam2': str(match.idTeam2.iconURL),
'nameTeam2': str(match.idTeam2.name),
'abbrTeam2': str(match.idTeam2.abbr),
'started': match_started,
'finished': match.finished,
'md_name': match.matchday_name,
'matchResult': match_result,
'tippFormTeam1': form['tippTeam1'],
'tippFormTeam2': form['tippTeam2'],
'match_id': match.id,
}
matches.append(item)
return matches
def _format_match_result(points_team1, points_team2):
"""Format match result for display."""
if points_team1 == -1 or points_team2 == -1:
return "-:-"
return f"{points_team1}:{points_team2}"
def _build_mandant_scores(user, matches):
"""Build mandant dict with user scores and tips."""
mandants = RelUserMandant.objects.filter(user=user).values_list(
'mandant', flat=True
)
if not mandants:
return {}
mandant_dict = {}
# Fetch all mandants and related users
mandant_users = {}
for mandant in mandants:
mandant_obj = get_object_or_404(Mandant, id=mandant)
mandant_users[mandant] = RelUserMandant.objects.filter(
mandant=mandant
).select_related('user')
# Get all tips for all matches and users
match_ids = [m['match_id'] for m in matches]
user_ids = []
for mandant in mandant_users:
user_ids.extend([rel.user_id for rel in mandant_users[mandant]])
all_tipps = {
(tipp.tipperID_id, tipp.matchID_id): tipp
for tipp in Tipp.objects.filter(
tipperID_id__in=user_ids,
matchID_id__in=match_ids
)
}
# Build mandant dict
for mandant in mandants:
mandant_obj = Mandant.objects.get(id=mandant)
mandant_dict[mandant_obj.description] = []
for rel in mandant_users[mandant]:
mate_data = {
'mate': rel.user,
'tipps': [],
'sum_score': 0
}
# Get tips for this mate across all matches
for match in matches:
tipp = all_tipps.get((rel.user_id, match['match_id']))
if tipp:
if match['started']:
score = tipp.score if tipp.score is not None else '\u231B'
tip_str = f"{tipp.pointsTeam1}:{tipp.pointsTeam2}({score})"
mate_data['tipps'].append(tip_str)
if match['finished']:
mate_data['sum_score'] += tipp.score or 0
else:
mate_data['tipps'].append('\u2714')
else:
mate_data['tipps'].append('\u2717')
mandant_dict[mandant_obj.description].append(mate_data)
# Sort by score
mandant_dict[mandant_obj.description] = sorted(
mandant_dict[mandant_obj.description],
key=lambda k: k['sum_score'],
reverse=True
)
return mandant_dict
def _get_mandant_posts(user):
"""Get latest published blog posts for user's mandants."""
mandants = RelUserMandant.objects.filter(user=user).values_list(
'mandant', flat=True
)
post_ids = RelPostMandant.objects.filter(
mandant__in=mandants
).values_list('post', flat=True)
posts = []
for post in Post.objects.filter(
id__in=post_ids,
published=True
).select_related('author__userprofile')[:5]:
avatar = None
try:
avatar = post.author.userprofile.avatar.name
except (ObjectDoesNotExist, AttributeError):
pass
posts.append((post, avatar))
return posts
@login_required @login_required
def charts(request, ls, season, pos='default', def charts(request, ls, season, pos='default',
template_name='charts.html'): template_name='charts.html'):