kategorie list i wykresy
This commit is contained in:
141
app.py
141
app.py
@@ -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
|
||||
)
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user