12 Commits

Author SHA1 Message Date
7b457475b4 Merge branch 'frontend-fix' 2025-06-13 00:34:11 +02:00
8fb2a22c2a another one 2025-06-13 00:32:13 +02:00
d1f5f8f3f4 Merge branch 'frontend' 2025-06-13 00:31:57 +02:00
56e68ed751 fix: show only relevant matches for a given year 2025-06-13 00:31:19 +02:00
f96a3ed1f1 fix 2025-06-12 23:42:55 +02:00
416b2ccfe0 changing responsive design 2025-06-12 23:08:58 +02:00
00bbe05b1c feat: include club's full name in matches view 2025-06-12 22:35:04 +02:00
e373c6b274 hotfix: fixes /mecze endpoint not working (cause of a wrong API call) 2025-06-12 20:43:22 +02:00
e80ee6bf8f feat: return matches with respect to received sportsman id
also include a comment in the database model to include a relation
2025-06-12 20:36:10 +02:00
a25be482b0 matches from DB 2025-06-12 16:23:14 +02:00
4e8cb48c82 fix: safe_traverse renaming issue 2025-06-11 00:38:55 +02:00
bc82fcd6b8 chore: depend on safe_traverse from lewy_globals, add scraper cron job 2025-06-11 00:35:48 +02:00
9 changed files with 166 additions and 38 deletions

View File

@@ -1,23 +1,13 @@
from flask import session from flask import session
from lewy_db import baza as ldb from lewy_db import baza as ldb
from lewy_globals import colors as c from lewy_globals import colors as c
from lewy_globals import safeTraverse as safe_traverse
import json import json
import lewy_globals import lewy_globals
import requests import requests
import time import time
from sqlalchemy import func from sqlalchemy import func
def safe_traverse(obj: dict, path: list, default=None):
result = obj
try:
for x in path:
result = result[x]
except (KeyError, TypeError):
result = default
print(f"safe_traverse: error reading {' -> '.join(path)} - returning: {default}")
finally:
return result
class scraper: class scraper:
headers = { headers = {

View File

@@ -1,7 +1,7 @@
from argparse import ArgumentParser from argparse import ArgumentParser
from flask import Flask, Response, render_template from flask import Flask, Response, render_template
from flask_apscheduler import APScheduler from flask_apscheduler import APScheduler
from fs_scraper import scraper as scr from fs_scraper import scraper
from lewy_globals import colors as c from lewy_globals import colors as c
import lewy_api import lewy_api
import lewy_db import lewy_db
@@ -13,7 +13,7 @@ import time
app = Flask(__name__) app = Flask(__name__)
app_host = "None" app_host = "None"
app_port = "None" app_port = "None"
scrape = None scr = None
def setup(): def setup():
# sanity check: make sure config is set # sanity check: make sure config is set
@@ -79,7 +79,7 @@ def setup():
app.add_url_rule('/api/<path:received_request>', view_func=lewy_api.api_global_catchall) app.add_url_rule('/api/<path:received_request>', view_func=lewy_api.api_global_catchall)
db = lewy_globals.setupDb(app, config) db = lewy_globals.setupDb(app, config)
scraper = scr() scr = scraper()
with app.app_context(): with app.app_context():
db.create_all() db.create_all()
@@ -97,8 +97,7 @@ def every5seconds():
def every2hours(): def every2hours():
# zaktualizuj bazę danych scrapując FS # zaktualizuj bazę danych scrapując FS
# ... scr.aktualizuj_dane()
# scraper.aktualizuj_dane()
return return
@app.route('/<string:val>', methods=['GET']) @app.route('/<string:val>', methods=['GET'])

View File

@@ -50,6 +50,16 @@ def not_implemented(data):
# TODO: change list to string -> data, not data[0] # TODO: change list to string -> data, not data[0]
return 501, f"not recognised/implemented: {data[0]}", [] return 501, f"not recognised/implemented: {data[0]}", []
def __czy_x_istnieje(typ, **id):
rekord = getDb().simple_select_all(typ, **id)
if rekord is not None and rekord != []:
return True
else:
return False
def czy_sportowiec_istnieje(id_zawodnika: str):
return __czy_x_istnieje("sportowcy", id_zawodnika=id_zawodnika)
# GET /api/v1 # GET /api/v1
def stub_hello(): def stub_hello():
""" """
@@ -85,15 +95,42 @@ def stats():
return 200, "ok", data_to_send return 200, "ok", data_to_send
# GET /api/v1/matches # GET /api/v1/matches
def get_matches(r): def get_matches(r = None, id_zawodnika: str | None = None, rok: int | None = None):
""" """
TODO: Zwraca mecze. Zwraca mecze.
Przykład wywołania:
get_matches(r, id_zawodnika=1), tożsame z GET /api/v1/matches?id_zawodnika=1
get_matches(r, rok=2024), tożsame z GET /api/v1/matches?rok=2024
get_matches(r), tożsame z GET /api/v1/matches
""" """
response_json = [] response_json = []
mecze = getDb().simple_select_all("mecze") mecze = None
if id_zawodnika is None:
# Gdy nie podano id wprost, sprawdź, czy podano je przez parametr.
id_zawodnika = r.args.get('id_zawodnika', -1)
if rok is None:
# Gdy nie podano roku wprost, sprawdź, czy podano je przez parametr.
# Jeśli nie, przyjmij None (2025).
rok = r.args.get('rok', None)
# Sprawdź, czy podano jakiekolwiek ID sportowca. Jeżeli nie, wypisz wszystkie mecze.
if id_zawodnika == -1:
mecze = getDb().get_sportsman_matches(year=rok)
# Sprawdź, czy sportowiec o podanym (lub niepodanym) id istnieje.
# Jeśli nie istnieje, wypisz wszystkie mecze.
elif not czy_sportowiec_istnieje(id_zawodnika=id_zawodnika):
return 404, "error", {"error_msg": "This sportsman has not been found in the database. Try: id_zawodnika=1"}
# Gdy sportowiec istnieje, wypisz jego mecze.
else:
mecze = getDb().get_sportsman_matches(id_zawodnika=id_zawodnika, year=rok)
for mecz in mecze: for mecz in mecze:
response_json.append(mecz.jsonify()) response_json.append(mecz.jsonify())
print(response_json) # print(f"zwracam mecze: {response_json}")
return 200, "ok", response_json return 200, "ok", response_json
# GET /api/v1/debugger_halt?token=XXX... # GET /api/v1/debugger_halt?token=XXX...

View File

@@ -1,7 +1,7 @@
from datetime import datetime from datetime import datetime
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from functools import wraps from functools import wraps
from sqlalchemy import ForeignKey, select, insert, update from sqlalchemy import ForeignKey, select, insert, update, extract
from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase, Session, relationship from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase, Session, relationship
from typing import List from typing import List
import time import time
@@ -95,7 +95,7 @@ class baza():
id_zawodnika: Mapped[ int] = mapped_column(ForeignKey(f"{tnp}sportowcy.id_zawodnika")) id_zawodnika: Mapped[ int] = mapped_column(ForeignKey(f"{tnp}sportowcy.id_zawodnika"))
zawodnik: Mapped[ "sportowcy"] = relationship(back_populates="mecze_zawodnika") zawodnik: Mapped[ "sportowcy"] = relationship(back_populates="mecze_zawodnika")
zewnetrzne_id_meczu: Mapped[ str] = mapped_column(ForeignKey(f"{tnp}mecze.zewnetrzne_id_meczu")) zewnetrzne_id_meczu: Mapped[ str] = mapped_column(ForeignKey(f"{tnp}mecze.zewnetrzne_id_meczu"))
mecz: Mapped[ "mecze"] = relationship() mecz: Mapped[ "mecze"] = relationship() # back_populates="zawodnicy_w_meczu", foreign_keys=[zewnetrzne_id_meczu])
czas_gry: Mapped[ int] = mapped_column() czas_gry: Mapped[ int] = mapped_column()
goli: Mapped[ int] = mapped_column() goli: Mapped[ int] = mapped_column()
asyst: Mapped[ int] = mapped_column() asyst: Mapped[ int] = mapped_column()
@@ -159,6 +159,8 @@ class baza():
skrocona_nazwa_turnieju: Mapped[ str] = mapped_column() skrocona_nazwa_turnieju: Mapped[ str] = mapped_column()
flaga: Mapped[ int] = mapped_column() flaga: Mapped[ int] = mapped_column()
# zawodnicy_w_meczu: Mapped[ List["sportowcy_w_meczach"]] = relationship(back_populates="mecz")
def __repr__(self): def __repr__(self):
return f"<Mecz #{self.id_meczu} ({self.zewnetrzne_id_meczu}, {self.gospodarze.skrocona_nazwa} vs. {self.goscie.skrocona_nazwa})>" return f"<Mecz #{self.id_meczu} ({self.zewnetrzne_id_meczu}, {self.gospodarze.skrocona_nazwa} vs. {self.goscie.skrocona_nazwa})>"
@@ -169,8 +171,10 @@ class baza():
"data": self.data.strftime("%Y-%m-%d"), "data": self.data.strftime("%Y-%m-%d"),
"gospodarze_id": self.gospodarze_id, "gospodarze_id": self.gospodarze_id,
"gospodarze": self.gospodarze.skrocona_nazwa, "gospodarze": self.gospodarze.skrocona_nazwa,
"gospodarze_pelna_nazwa": self.gospodarze.pelna_nazwa,
"goscie_id": self.goscie_id, "goscie_id": self.goscie_id,
"goscie": self.goscie.skrocona_nazwa, "goscie": self.goscie.skrocona_nazwa,
"goscie_pelna_nazwa": self.goscie.pelna_nazwa,
"gosp_wynik": self.gosp_wynik, "gosp_wynik": self.gosp_wynik,
"gosc_wynik": self.gosc_wynik, "gosc_wynik": self.gosc_wynik,
"sezon": self.sezon, "sezon": self.sezon,
@@ -494,18 +498,58 @@ class baza():
if zewnetrzne_id_zawodnika is not None: if zewnetrzne_id_zawodnika is not None:
id_zawodnika = self.get_id_zawodnika_by_zewnetrzne_id(zewnetrzne_id_zawodnika) id_zawodnika = self.get_id_zawodnika_by_zewnetrzne_id(zewnetrzne_id_zawodnika)
# Aliasy (dla czytelności)
SportowcyWMeczach = self.entities["sportowcy_w_meczach"]
Sportowcy = self.entities["sportowcy"]
query = self.session.query( query = self.session.query(
self.entities["sportowcy_w_meczach"] SportowcyWMeczach
).join( ).join(
self.entities["sportowcy_w_meczach"].zawodnik SportowcyWMeczach.zawodnik
).filter( ).filter(
self.entities["sportowcy"].id_zawodnika == id_zawodnika Sportowcy.id_zawodnika == id_zawodnika
) )
#print(query) # print(f"get_sportowcy_w_meczach_by_sportsman_id: {query}")
if order.lower() == "desc": if order.lower() == "desc":
query = query.order_by(self.entities["sportowcy_w_meczach"].id_rekordu.desc()) query = query.order_by(SportowcyWMeczach.id_rekordu.desc())
return query.all()
@exit_gracefully
def get_sportsman_matches(self, id_zawodnika = None, zewnetrzne_id_zawodnika = None, order = "DESC", year = None):
# Spróbuj otrzymać id zawodnika z zewnętrznego id.
if zewnetrzne_id_zawodnika is not None:
id_zawodnika = self.get_id_zawodnika_by_zewnetrzne_id(zewnetrzne_id_zawodnika)
# Aliasy
Mecze = self.entities["mecze"]
SportowcyWMeczach = self.entities["sportowcy_w_meczach"]
Sportowcy = self.entities["sportowcy"]
query = self.session.query(
Mecze
).join(
SportowcyWMeczach, Mecze.zewnetrzne_id_meczu == SportowcyWMeczach.zewnetrzne_id_meczu
)
if id_zawodnika is not None:
query = query.join(
Sportowcy, SportowcyWMeczach.id_zawodnika == Sportowcy.id_zawodnika
).filter(
Sportowcy.id_zawodnika == id_zawodnika
)
if year is not None:
query = query.filter(
extract("year", Mecze.data) == year
)
if order.lower() == "desc":
query = query.order_by(Mecze.data.desc())
# print(f"get_sportsman_matches: {query}")
return query.all() return query.all()

View File

@@ -23,9 +23,9 @@ def safeTraverse(obj: dict, path: list, default=None):
try: try:
for x in path: for x in path:
result = result[x] result = result[x]
except KeyError: except (KeyError, TypeError):
result = default result = default
# print(f"error reading: {' -> '.join(path)} - returning: {default}") print(f"error reading: {' -> '.join(path)} - returning: {default}")
finally: finally:
return result return result

View File

@@ -3,7 +3,7 @@ import lewy_api_v1
import lewy_db import lewy_db
import lewy_globals import lewy_globals
import json import json
from lewy_api_v1 import get_matches
def get_lewy_stats(): def get_lewy_stats():
return { return {
'all_time_stats': { 'all_time_stats': {
@@ -60,9 +60,13 @@ def index():
def mecze(): def mecze():
# Możesz dostarczyć szczegóły dotyczące meczów # Możesz dostarczyć szczegóły dotyczące meczów
selected_date = request.args.get("date", '2025') selected_date = request.args.get("date", '2025')
with open("static/lewandowski_matches.json", "r") as file: try:
data = json.load(file) selected_date = int(selected_date)
matches = data["data"] except:
selected_date = 2025
#with open("static/lewandowski_matches.json", "r") as file:
# data = json.load(file)
status, msg, matches = get_matches(None, id_zawodnika=1, rok=selected_date)
return render_template('matches.html', matches=matches, selected_date=selected_date) return render_template('matches.html', matches=matches, selected_date=selected_date)

View File

@@ -386,6 +386,34 @@ header button {
display: none; display: none;
} }
} }
@media (max-width: 600px){
.section__matches
{
font-size: 10px;
}
.section__matches th{
padding: 3px;
}
.club-stats-grid
{
grid-template-columns: 1fr 1fr !important;
}
.club-stats-grid .stat-box
{
width: 100%;
}
.section-stats-center .section-stats .stats
{
display: flex;
flex-direction: column;
align-items: center;
}
.section-stats-center .section-stats .stats .stat-box
{
width: 100%;
padding: 0;
}
}
@@ -878,4 +906,32 @@ select
background-color: var(--barca-blue); background-color: var(--barca-blue);
border-radius: 10px; border-radius: 10px;
border: 2px solid white; border: 2px solid white;
}
@media (max-width: 600px){
.sectionmatches
{
font-size: 10px;
}
.sectionmatches th{
padding: 3px;
}
.club-stats-grid
{
grid-template-columns: 1fr 1fr !important;
}
.club-stats-grid .stat-box
{
width: 100%;
}
.section-stats-center .section-stats .stats
{
display: flex;
flex-direction: column;
align-items: center;
}
.section-stats-center .section-stats .stats .stat-box
{
width: 100%;
padding: 0;
}
} }

View File

@@ -51,7 +51,7 @@
</div> </div>
<div class="stat-box"> <div class="stat-box">
<h3>{{ player.assists}}</h3> <h3>{{ player.matches}}</h3>
</div> </div>
</div> </div>

View File

@@ -36,20 +36,18 @@
<th></th> <th></th>
</tr> </tr>
{% for match in matches %} {% for match in matches %}
{% if match.data[:4] == selected_date %}
<tr> <tr>
<td>{{ match.data }}</td> <td>{{ match.data }}</td>
<td>{{ match.gospodarze_id }}</td> <td>{{ match.gospodarze_pelna_nazwa }}</td>
<td>{{ match.gosp_wynik }} : {{ match.gosc_wynik }}</td> <td>{{ match.gosp_wynik }} : {{ match.gosc_wynik }}</td>
<td>{{ match.goscie_id }}</td> <td>{{ match.goscie_pelna_nazwa }}</td>
<!-- <!--
<td>{{ match.goals }}</td> <td>{{ match.goals }}</td>
<td>{{ match.assists }}</td> <td>{{ match.assists }}</td>
<td>{{ match.minutes }}</td> <td>{{ match.minutes }}</td>
--> -->
</tr> </tr>
{% endif %}
{% endfor %} {% endfor %}
</table> </table>
</section> </section>