kategorie list i wykresy

This commit is contained in:
Mateusz Gruszczyński
2025-07-30 23:20:03 +02:00
parent 437f7a26e3
commit 978bcbe051
12 changed files with 711 additions and 32 deletions

141
app.py
View File

@@ -163,6 +163,17 @@ class User(UserMixin, db.Model):
is_admin = db.Column(db.Boolean, default=False)
# Tabela pośrednia
shopping_list_category = db.Table(
"shopping_list_category",
db.Column("shopping_list_id", db.Integer, db.ForeignKey("shopping_list.id"), primary_key=True),
db.Column("category_id", db.Integer, db.ForeignKey("category.id"), primary_key=True)
)
class Category(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
class ShoppingList(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(150), nullable=False)
@@ -173,7 +184,6 @@ class ShoppingList(db.Model):
is_temporary = db.Column(db.Boolean, default=False)
share_token = db.Column(db.String(64), unique=True, nullable=True)
# expires_at = db.Column(db.DateTime, nullable=True)
expires_at = db.Column(db.DateTime(timezone=True), nullable=True)
owner = db.relationship("User", backref="lists", lazy=True)
is_archived = db.Column(db.Boolean, default=False)
@@ -184,6 +194,12 @@ class ShoppingList(db.Model):
receipts = db.relationship("Receipt", back_populates="shopping_list", lazy="select")
expenses = db.relationship("Expense", back_populates="shopping_list", lazy="select")
# Nowa relacja wiele-do-wielu
categories = db.relationship(
"Category",
secondary=shopping_list_category,
backref=db.backref("shopping_lists", lazy="dynamic")
)
class Item(db.Model):
id = db.Column(db.Integer, primary_key=True)
@@ -301,28 +317,53 @@ if app.config["SQLALCHEMY_DATABASE_URI"].startswith("sqlite:///"):
with app.app_context():
db.create_all()
# --- Tworzenie admina ---
admin_username = DEFAULT_ADMIN_USERNAME
admin_password = DEFAULT_ADMIN_PASSWORD
password_hash = hash_password(admin_password)
# Szukamy użytkownika o loginie "admin"
admin = User.query.filter_by(username=admin_username).first()
if admin:
if not admin.is_admin:
admin.is_admin = True # Ustaw admina jeśli był user ale nie admin
admin.is_admin = True
if not check_password(admin.password_hash, admin_password):
admin.password_hash = password_hash
print(f"[INFO] Zmieniono hasło admina '{admin_username}' z konfiguracji.")
db.session.commit()
else:
# Tworzymy tylko jeśli NIE istnieje taki username!
admin = User(
username=admin_username, password_hash=password_hash, is_admin=True
)
db.session.add(admin)
db.session.add(User(
username=admin_username,
password_hash=password_hash,
is_admin=True
))
db.session.commit()
# --- Predefiniowane kategorie ---
default_categories = [
"Spożywcze", "Budowlane", "Zabawki", "Chemia", "Inne",
"Elektronika", "Odzież i obuwie", "Artykuły biurowe",
"Kosmetyki i higiena", "Motoryzacja", "Ogród i rośliny",
"Zwierzęta", "Sprzęt sportowy", "Książki i prasa",
"Narzędzia i majsterkowanie", "RTV / AGD", "Apteka i suplementy",
"Artykuły dekoracyjne", "Gry i hobby", "Usługi"
]
# Pobierz istniejące nazwy z bazy, ignorując puste/niewłaściwe rekordy
existing_names = {
c.name for c in Category.query.filter(Category.name.isnot(None)).all()
}
# Znajdź brakujące
missing = [cat for cat in default_categories if cat not in existing_names]
# Dodaj tylko brakujące
if missing:
db.session.add_all(Category(name=cat) for cat in missing)
db.session.commit()
print(f"[INFO] Dodano brakujące kategorie: {', '.join(missing)}")
else:
print("[INFO] Wszystkie domyślne kategorie już istnieją")
@static_bp.route("/static/js/<path:filename>")
def serve_js(filename):
@@ -399,6 +440,14 @@ def get_total_expense_for_list(list_id, start_date=None, end_date=None):
return query.scalar() or 0
def update_list_categories_from_form(shopping_list, form):
category_ids = form.getlist("categories")
shopping_list.categories.clear()
if category_ids:
cats = Category.query.filter(Category.id.in_(category_ids)).all()
shopping_list.categories.extend(cats)
def generate_share_token(length=8):
return secrets.token_hex(length // 2)
@@ -588,10 +637,10 @@ def get_total_expenses_grouped_by_list_created_at(
start_date=None,
end_date=None,
user_id=None,
category_id=None,
):
lists_query = ShoppingList.query
# Uprawnienia
if admin:
pass
elif show_all:
@@ -604,7 +653,12 @@ def get_total_expenses_grouped_by_list_created_at(
else:
lists_query = lists_query.filter(ShoppingList.owner_id == user_id)
# Filtr daty utworzenia listy
if category_id:
lists_query = lists_query.join(
shopping_list_category,
shopping_list_category.c.shopping_list_id == ShoppingList.id
).filter(shopping_list_category.c.category_id == category_id)
if start_date and end_date:
try:
dt_start = datetime.strptime(start_date, "%Y-%m-%d")
@@ -1190,6 +1244,9 @@ def edit_my_list(list_id):
if l.owner_id != current_user.id:
abort(403, description="Nie jesteś właścicielem tej listy.")
categories = Category.query.order_by(Category.name.asc()).all()
selected_categories_ids = {c.id for c in l.categories}
if request.method == "POST":
# Obsługa zmiany miesiąca utworzenia listy
move_to_month = request.form.get("move_to_month")
@@ -1236,11 +1293,21 @@ def edit_my_list(list_id):
else:
l.expires_at = None
# Obsługa wyboru kategorii
update_list_categories_from_form(l, request.form)
db.session.commit()
flash("Zaktualizowano dane listy", "success")
return redirect(url_for("main_page"))
return render_template("edit_my_list.html", list=l, receipts=receipts)
return render_template(
"edit_my_list.html",
list=l,
receipts=receipts,
categories=categories,
selected_categories=selected_categories_ids
)
@app.route("/delete_user_list/<int:list_id>", methods=["POST"])
@@ -1381,7 +1448,10 @@ def view_list(list_id):
def user_expenses():
start_date_str = request.args.get("start_date")
end_date_str = request.args.get("end_date")
show_all = request.args.get("show_all", "false").lower() == "true"
category_id = request.args.get("category_id", type=int)
show_all = request.args.get("show_all", "true").lower() == "true"
categories = Category.query.order_by(Category.name.asc()).all()
start = None
end = None
@@ -1389,19 +1459,25 @@ def user_expenses():
expenses_query = Expense.query.options(
joinedload(Expense.shopping_list).joinedload(ShoppingList.owner),
joinedload(Expense.shopping_list).joinedload(ShoppingList.expenses),
joinedload(Expense.shopping_list).joinedload(ShoppingList.categories),
).join(ShoppingList, Expense.list_id == ShoppingList.id)
# Filtry dostępu
if not show_all:
expenses_query = expenses_query.filter(ShoppingList.owner_id == current_user.id)
else:
expenses_query = expenses_query.filter(
or_(
ShoppingList.owner_id == current_user.id, ShoppingList.is_public == True
ShoppingList.owner_id == current_user.id,
ShoppingList.is_public == True
)
)
# Filtr daty
if category_id:
expenses_query = expenses_query.join(
shopping_list_category,
shopping_list_category.c.shopping_list_id == ShoppingList.id
).filter(shopping_list_category.c.category_id == category_id)
if start_date_str and end_date_str:
try:
start = datetime.strptime(start_date_str, "%Y-%m-%d")
@@ -1412,10 +1488,8 @@ def user_expenses():
except ValueError:
flash("Błędny zakres dat", "danger")
# Pobranie wszystkich wydatków z powiązanymi listami
expenses = expenses_query.order_by(Expense.added_at.desc()).all()
# Zbiorcze sumowanie wydatków per lista w SQL
list_ids = {e.list_id for e in expenses}
totals_map = {}
if list_ids:
@@ -1439,7 +1513,7 @@ def user_expenses():
for e in expenses
]
# Lista z danymi i sumami
# Lista z danymi i kategoriami (dla JS)
lists_data = [
{
"id": l.id,
@@ -1447,6 +1521,7 @@ def user_expenses():
"created_at": l.created_at,
"total_expense": totals_map.get(l.id, 0),
"owner_username": l.owner.username if l.owner else "?",
"categories": [c.id for c in l.categories]
}
for l in {e.shopping_list for e in expenses if e.shopping_list}
]
@@ -1455,6 +1530,8 @@ def user_expenses():
"user_expenses.html",
expense_table=expense_table,
lists_data=lists_data,
categories=categories,
selected_category=category_id,
show_all=show_all,
)
@@ -1465,7 +1542,8 @@ def user_expenses_data():
range_type = request.args.get("range", "monthly")
start_date = request.args.get("start_date")
end_date = request.args.get("end_date")
show_all = request.args.get("show_all", "false").lower() == "true"
show_all = request.args.get("show_all", "true").lower() == "true"
category_id = request.args.get("category_id", type=int)
result = get_total_expenses_grouped_by_list_created_at(
user_only=True,
@@ -1475,7 +1553,9 @@ def user_expenses_data():
start_date=start_date,
end_date=end_date,
user_id=current_user.id,
category_id=category_id
)
if "error" in result:
return jsonify({"error": result["error"]}), 400
return jsonify(result)
@@ -2200,15 +2280,16 @@ def delete_selected_lists():
@admin_required
def edit_list(list_id):
# Pobieramy listę z powiązanymi danymi jednym zapytaniem
l = (
db.session.query(ShoppingList)
.options(
l = db.session.get(
ShoppingList,
list_id,
options=[
joinedload(ShoppingList.expenses),
joinedload(ShoppingList.receipts),
joinedload(ShoppingList.owner),
joinedload(ShoppingList.items),
)
.get(list_id)
joinedload(ShoppingList.categories),
]
)
if l is None:
@@ -2217,6 +2298,9 @@ def edit_list(list_id):
# Suma wydatków z listy
total_expense = get_total_expense_for_list(l.id)
categories = Category.query.order_by(Category.name.asc()).all()
selected_categories_ids = {c.id for c in l.categories}
if request.method == "POST":
action = request.form.get("action")
@@ -2285,11 +2369,14 @@ def edit_list(list_id):
)
return redirect(url_for("edit_list", list_id=list_id))
# aktualizacja kategorii
update_list_categories_from_form(l, request.form)
db.session.add(l)
db.session.commit()
flash("Zapisano zmiany listy", "success")
return redirect(url_for("edit_list", list_id=list_id))
elif action == "add_item":
item_name = request.form.get("item_name", "").strip()
quantity_str = request.form.get("quantity", "1")
@@ -2397,6 +2484,8 @@ def edit_list(list_id):
users=users,
items=items,
receipts=receipts,
categories=categories,
selected_categories=selected_categories_ids
)