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
*.swp
bin/
manage.py
# Virtuelle Umgebung
venv/
env/
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/
old_stuff/
static/
templates/blogindex.html-media
tippy/static/
warnings.txt
staticfiles/
# IDE
.vscode/
.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'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
@@ -28,9 +27,7 @@ DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
@@ -77,10 +74,9 @@ WSGI_APPLICATION = 'TipPy.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'tippy',
'USER': 'tippy',
'PASSWORD': 'tippy',
@@ -91,7 +87,6 @@ DATABASES = {
# Password validation
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
@@ -109,13 +104,9 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/
LANGUAGE_CODE = 'de-de'
TIME_ZONE = 'Europe/Berlin'
USE_I18N = True
USE_TZ = True
# Absolute filesystem path to the directory that will hold user-uploaded files.
@@ -149,7 +140,6 @@ STATICFILES_DIRS = [
# Default primary key field type
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# 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
psycopg2~=2.9.3
suds~=1.1.2
Pillow~=9.2.0
pytz~=2022.1
urllib3~=1.26.10
asgiref==3.12.1
Django==6.1.1
pillow==12.3.0
psycopg2==2.9.13
pytz==2026.3.post1
sqlparse==0.6.0
suds-community==1.2.0
urllib3==2.8.0
+2 -2
View File
@@ -36,7 +36,7 @@
<body>
<!-- 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 -->
<script src="{% static 'js/bootstrap.min.js'%}"></script>
@@ -53,7 +53,7 @@
<div class="col-md-7">
<p>
<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
Spielergebnisse.
</div>
+9 -14
View File
@@ -12,8 +12,8 @@
{% block body %}
<div id="content-main">
<div class="row">
<div class="col-md-7">
<h2>Deine Tipps</h2>
<div class="col-md-6">
<h3>Deine Tipps</h3>
<nav>
<ul class="pager">
<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
</button>
</form>
</div>
</div>
<div class="row">
{% for mandant, list in mandant_dict.items %}
<div class="col-md-7">
<h4>Mitspieler {{ mandant }}</h4>
{% for mandant, list in mandant_dict.items %}
<h3>Mitspieler {{ mandant }}</h3>
<div class="table-responsive">
<table class="table table-hover table-condensed">
<tr>
@@ -105,12 +102,10 @@
</tr>
</table>
</div>
</div>
{% endfor %}
</div>
<div class="row">
<div class="col-md-7">
<h2>Chat</h2>
{% endfor %}
</div>
<div class="col-md-6">
<h3>Chat</h3>
{% block chat %}
{% include "chat.xhtml" with posts=posts %}
{% endblock %}
+3
View File
@@ -16,6 +16,9 @@
<li><a href="/">Tipps</a></li>
<li><a href="/charts/{{ ls }}/{{ season }}{% if pos %}/{{ pos }}{% endif %}">Bestenliste</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 class="nav navbar-nav navbar-right">
{% 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/>.
"""
from django.db import models
from django.contrib.auth.models import User
from django.db import models
class UserProfile(models.Model):
user = models.OneToOneField(
@@ -31,13 +31,18 @@ class UserProfile(models.Model):
def __unicode__(self):
return self.user.username
class Competition(models.Model):
leagueShortcut = models.CharField(max_length=32)
leagueName = models.CharField(max_length=128)
season = models.CharField(max_length=4)
class Meta:
unique_together = ("leagueShortcut", "season")
def __unicode__(self):
return(self.leagueShortcut)
return (self.leagueShortcut)
class Team(models.Model):
teamID = models.IntegerField(unique=True, primary_key=True)
@@ -55,11 +60,17 @@ class Team(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()
group = models.IntegerField()
matchday = models.IntegerField()
matchday_name = models.CharField(max_length=128)
competition = models.ForeignKey(
Competition,
on_delete=models.CASCADE
)
idTeam1 = models.ForeignKey(
Team, related_name='+',
on_delete=models.CASCADE
@@ -68,15 +79,15 @@ class Match(models.Model):
Team, related_name='+',
on_delete=models.CASCADE
)
pointsTeam1 = models.SmallIntegerField()
pointsTeam2 = models.SmallIntegerField()
finished = models.BooleanField()
season = models.CharField(max_length=4)
leagueShortcut = models.CharField(max_length=12)
def __unicode__(self):
return (str(self.matchID))
class Tipp(models.Model):
matchID = models.ForeignKey(
Match,
@@ -95,12 +106,15 @@ class Tipp(models.Model):
class Meta:
unique_together = ("matchID", "tipperID")
class Mandant(models.Model):
name = models.CharField(max_length=32, unique=True)
description = models.CharField(max_length=255)
def __unicode__(self):
return self.name
class Score(models.Model):
client = models.ForeignKey(
Mandant,
@@ -110,10 +124,12 @@ class Score(models.Model):
exact = models.PositiveSmallIntegerField()
diff = models.PositiveSmallIntegerField()
tendency = models.PositiveSmallIntegerField()
def __unicode__(self):
return str(self.client)
class chart(models.Model):
class Chart(models.Model):
team = models.ForeignKey(
Team,
on_delete=models.CASCADE
@@ -129,6 +145,7 @@ class chart(models.Model):
class Meta:
unique_together = ("team", "competition")
class RelUserMandant(models.Model):
user = models.ForeignKey(
User,
@@ -141,9 +158,11 @@ class RelUserMandant(models.Model):
class Meta:
unique_together = ("user", "mandant")
def __unicode__(self):
return (str(self.user) + " -> " + str(self.mandant))
class Post(models.Model):
content = models.TextField()
published = models.BooleanField(default=True)
@@ -159,6 +178,7 @@ class Post(models.Model):
def __unicode__(self):
return u'%s' % self.created
class RelPostMandant(models.Model):
post = models.ForeignKey(
Post,
@@ -171,6 +191,6 @@ class RelPostMandant(models.Model):
class Meta:
unique_together = ("post", "mandant")
def __unicode__(self):
return (str(self.post) + " -> " + str(self.mandant))
+29 -40
View File
@@ -18,20 +18,22 @@
# You should have received a copy of the GNU General Public License
# along with TipPy. If not, see <http://www.gnu.org/licenses/>.
#
import sys
import locale
from suds.client import Client
import urllib.request
import urllib.error
import urllib.parse
import psycopg2
import urllib.request
from zeep import Client
from zeep.transports import Transport
from requests import Session
class OpenLiga(object):
version = "0.1"
error = ""
proxyurl = None
debug = None
client = None
version = "0.1"
error = ""
proxyurl = None
debug = None
client = None
def __init__(self, proxyurl=None, debug=False):
@@ -40,57 +42,44 @@ class OpenLiga(object):
self.debug = debug
# get a SUDS client object
if self.proxyurl is None:
try:
self.client = Client(
'http://www.openligadb.de/'
+ 'Webservices/Sportsdata.asmx?WSDL')
except (urllib.error.URLError):
self.error += "Connect to webservice failed."
else:
try:
t = suds.transport.http.HttpTransport()
proxy = urllib.request.ProxyHandler({'http':proxyurl})
opener = urllib.request.build_opener(proxy)
t.urlopener = opener
self.client = suds.client.Client(
'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"
# get a SOAP client object
wsdl_url = 'http://www.openligadb.de/Webservices/Sportsdata.asmx?WSDL'
try:
if proxyurl is None:
self.client = Client(wsdl=wsdl_url)
else:
session = Session()
session.proxies = {'http': proxyurl, 'https': proxyurl}
transport = Transport(session=session)
self.client = Client(wsdl=wsdl_url, transport=transport)
except (urllib.error.URLError, Exception) as e:
self.error += f"Connect to webservice failed: {str(e)}\n"
def getSeason(self, season, league='bl1'):
""" Get the whole season.
Args: season and league shortcut (optional)
"""
return(self.client.service.GetMatchdataByLeagueSaison(
leagueShortcut=league,leagueSaison=season)
)
return self.client.service.GetMatchdataByLeagueSaison(
leagueShortcut=league, leagueSaison=season)
def getMatchday(self, season, matchdaynumber, league='bl1'):
""" Get matchday.
Args: season, matchdaynumber and league shortcut (optional)
"""
return(self.client.service.GetMatchdataByGroupLeagueSaison(
return self.client.service.GetMatchdataByGroupLeagueSaison(
groupOrderID=matchdaynumber,
leagueShortcut=league,
leagueSaison=season)
)
def getTeams(self, season, league='bl1'):
return(self.client.service.GetTeamsByLeagueSaison(
return self.client.service.GetTeamsByLeagueSaison(
leagueShortcut=league,
leagueSaison=season)
)
def getCurrentGroup(self, league):
return(self.client.service.GetCurrentGroup(
return self.client.service.GetCurrentGroup(
leagueShortcut=league)
)
+1
View File
@@ -22,6 +22,7 @@ urlpatterns = [
re_path(r'^blog/newpost$', views.newBlogpost),
re_path(r'^blog/(?P<page>\d)$', views.blogindex),
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/logout/', auth_views.LogoutView.as_view())
]
+523 -385
View File
@@ -1,6 +1,6 @@
# -*- 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.
@@ -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/>.
"""
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
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):
return django.shortcuts.render(request, 'about.html')
"""Render about page."""
return render(request, 'about.html')
@login_required
def home(request):
"""Redirect to current matchday."""
ls = get_current_ls()
season = get_current_season()
md = get_current_md(ls)
return django.shortcuts.redirect("matchday",
ls=ls,
season=season,
matchday=str(md).zfill(2)
)
return redirect(
'matchday',
ls=ls,
season=season,
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
@require_http_methods(["GET", "POST"])
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)
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':
user_form = UserForm(data=request.POST, instance=user)
profile_form = UserProfileForm(
data=request.POST, instance=profile
)
return _handle_profile_post(request, user, profile)
else:
return _handle_profile_get(request, user, profile)
if user_form.is_valid() and profile_form.is_valid():
user = user_form.save()
def _handle_profile_post(request, user, profile):
"""Handle POST request for profile update."""
user_form = UserForm(data=request.POST, instance=user)
profile_form = UserProfileForm(data=request.POST, instance=profile)
profile = profile_form.save(commit=False)
profile.user = user
if user_form.is_valid() and profile_form.is_valid():
user = user_form.save()
profile = profile_form.save(commit=False)
profile.user = user
# Did the user provide a profile picture?
if 'avatar' in request.FILES:
profile.avatar = request.FILES['avatar']
# Now we save the UserProfile model instance.
profile.save()
# create thumbnail
imfn = pjoin(settings.MEDIA_ROOT, profile.avatar.name)
im = PImage.open(imfn)
im.thumbnail((50, 50), PImage.ANTIALIAS)
im.save(settings.MEDIA_ROOT + profile.avatar.name)
if 'avatar-clear' in request.POST:
if 'on' in request.POST['avatar-clear']:
image = pjoin(settings.MEDIA_ROOT, img)
debug.append('Is the image ' + str(image)
+ ' a regular file? '
+ str(os.path.isfile(image))
)
if os.path.isfile(image):
os.remove(image)
# Handle avatar upload
if 'avatar' in request.FILES:
profile.avatar = request.FILES['avatar']
profile.save()
for item in request.POST:
debug.append(str(item) + " -> "
+ str(request.POST[item])
)
image_path = os.path.join(settings.MEDIA_ROOT, profile.avatar.name)
try:
_create_thumbnail(image_path)
except Exception as e:
logger.error(f"Thumbnail creation failed: {str(e)}")
# Invalid form or forms - mistakes or something else?
# Print problems to the terminal.
# They'll also be shown to the user.
else:
print(
user_form.errors,
profile_form.errors
)
# Handle avatar deletion
elif 'avatar-clear' in request.POST and request.POST['avatar-clear'] == 'on':
if profile.avatar:
old_image_path = os.path.join(settings.MEDIA_ROOT, profile.avatar.name)
_delete_avatar(old_image_path)
profile.avatar = None
return django.shortcuts.redirect('/accounts/profile/' + str(user.id))
# Not a HTTP POST, so we render our form using two ModelForm
# instances. These forms will be blank, ready for user input.
profile.save()
return redirect('profile', pk=user.id)
else:
user_form = UserForm(
initial={'last_name': user.last_name,
'first_name': user.first_name,
'email': user.email},
instance=user
)
profile_form = UserProfileForm(instance=profile)
logger.warning(f"Form validation failed for user {user.id}: "
f"User errors: {user_form.errors}, "
f"Profile errors: {profile_form.errors}")
return django.shortcuts.render(request, 'registration/profile.html', {
return _render_profile_form(request, user_form, profile_form, profile)
def _handle_profile_get(request, user, profile):
"""Handle GET request for profile display."""
user_form = UserForm(instance=user)
profile_form = UserProfileForm(instance=profile)
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,
'profile_form': profile_form,
'debug': debug,
'img': img}
)
'avatar': profile.avatar.name if profile.avatar else None,
}
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):
"""Fetch and import teams and matches for a season from OpenLiga."""
ol = OpenLiga()
teams = ol.getTeams(str(season), ls)
season = ol.getSeason(str(season), ls)
teams_data = ol.getTeams(str(season), ls)
season_data = ol.getSeason(str(season), ls)
return_vals = []
for team in teams[0]:
t = Team(
teamID=team['teamID'],
name=team['teamName'],
iconURL=team['teamIconURL']
# Bulk import teams
_import_teams(teams_data[0])
# Bulk import matches
_import_season_matches(season_data[0], ls)
return redirect(
'matchday',
ls=ls,
season=get_current_season(),
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']
)
)
"""
check if object exits and do insert if not
"""
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]:
m = Match(matchID=match['matchID'],
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,
season=get_current_season(),
matchday=str(get_current_md(ls)).zfill(2)
)
if matches:
Match.objects.bulk_create(matches, ignore_conflicts=True)
@login_required
def update(request, ls, season, cur_md):
"""
To do:
- update table tipp_competition
"""
"""Update match results and calculate user scores for current matchday."""
ol = OpenLiga()
md_matches = ol.getMatchday(str(season), int(cur_md), ls)
# Achtung: DateTimeField prüfen
for match in 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
# Update match results
_update_match_results(md_matches[0])
except:
pointsTeam1 = -1
pointsTeam2 = -1
# Calculate scores for all tips
_calculate_matchday_scores(request.user, ls, season, cur_md)
m = Match(matchID=match['matchID'],
matchDateTime=match['matchDateTime'],
pointsTeam1=pointsTeam1,
pointsTeam2=pointsTeam2,
finished=bool(match['matchIsFinished']),
)
m.save(update_fields=['matchDateTime', 'pointsTeam1', 'pointsTeam2', 'finished'])
pointsTeam1 = -1
pointsTeam2 = -1
return redirect('matchday', ls=ls, season=season, matchday=cur_md)
# evaluate scores
""" get mandants current user is related """
mandants = RelUserMandant.objects.filter(user=request.user). \
values_list('mandant', flat=True)
# scores = Score.objects.get(client=Mandant.objects.get(name='huse'))
scores = Score.objects.get(client=mandants[0])
def _update_match_results(matches_data):
"""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']),
)
)
if matches_to_update:
Match.objects.bulk_update(
matches_to_update,
['matchDateTime', 'pointsTeam1', 'pointsTeam2', 'finished'],
batch_size=100
)
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=True,
leagueShortcut=ls,
season=season,
matchday=cur_md
)
).prefetch_related('tipp_set')
tipps_to_update = []
for match in finished_matches:
tipps = Tipp.objects.filter(matchID=match.matchID)
for tipp in tipps:
score = 0
goals_total = match.pointsTeam1 + match.pointsTeam2
# Tipp is acurate
if (tipp.pointsTeam1 == match.pointsTeam1
and tipp.pointsTeam2 == match.pointsTeam2):
if goals_total >= 5:
score = scores.exact_high
else:
score = scores.exact
tipp.score = score
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
if tipp.pointsTeam1 > tipp.pointsTeam2:
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
for tipp in match.tipp_set.all():
score = _calculate_tipp_score(tipp, match, scores)
tipp.score = score
tipp.save()
continue
tipps_to_update.append(tipp)
return django.shortcuts.redirect("matchday",
ls=ls,
season=season,
matchday=cur_md
)
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
return scores.exact_high if goals_total >= 5 else scores.exact
# Winner and difference
winner_real = _get_winner(match.pointsTeam1, match.pointsTeam2)
winner_tipp = _get_winner(tipp.pointsTeam1, tipp.pointsTeam2)
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:
return scores.tendency
return 0
def _get_winner(points_team1, points_team2):
"""Determine match winner.
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
@require_http_methods(["GET", "POST"])
def matchday(request, ls, season, matchday, template_name='md.html'):
debug = ''
debug += "Debugging: "
has_refresh = True
"""Display matchday with user tips, match results, and mandant scores."""
# Process form submissions first
_process_matchday_tips(request, ls, season, matchday)
""" get matches of the matchday """
md_matches = Match.objects.filter(leagueShortcut=ls, season=season,
matchday=int(matchday)).order_by('matchDateTime')
user = request.user
matches = _build_matchday_matches(request, user, ls, season, matchday)
mandant_dict = _build_mandant_scores(user, matches)
posts = _get_mandant_posts(user)
""" get the current user """
user = User.objects.get(username=request.user.username)
cur_group = matches[0]['md_name'] if matches else "N/A"
""" get mandants current user is related """
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,
return render(request, template_name, {
'cur_md': matchday,
'md_name': cur_group,
'matches': matches,
'ls': ls,
'season': season,
'username': user,
'username': user.username,
'mandant_dict': mandant_dict,
# 'tipp_mates': sorted(tipp_mates, key=lambda k: k['sum_score'],
# reverse=True),
'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
def charts(request, ls, season, pos='default',
template_name='charts.html'):