diff --git a/app/api/items.py b/app/api/items.py new file mode 100644 index 0000000000000000000000000000000000000000..cac40ab37298ac179363b10f9e7d74d49282f471 --- /dev/null +++ b/app/api/items.py @@ -0,0 +1,313 @@ +from flask import Blueprint, request, jsonify +from ..token import token_required +from ..functions import is_numeric, validate_json_fields +from ..config import * +from ..database import db +from ..models import Icon, User, Item, Transaction + +ICON_DOES_NOT_EXISTS_MESSAGE = 'Nincs ilyen ikon!' +ITEM_DOES_NOT_EXISTS_MESSAGE = 'Nincs ilyen termék!' + +# Endpoint handeler blueprint létrehozása +items = Blueprint('items', __name__, url_prefix="/items") + +# /api/items +# GET # Termékek listáján ak lekérdezése +@items.route('/', methods=['GET']) +def get_items(): + """ + Termékek listázása + --- + tags: + - Termékek + summary: Az összes termék lekérdezése. + description: A rendszerben lévő összes termék listázása. + responses: + 200: + description: Sikeres lekérdezés. + content: + application/json: + schema: + type: array + items: + type: object + properties: + id: + type: integer + name: + type: string + price: + type: integer + unit: + type: string + icon_id: + type: string + 400: + description: Hibás kérés. + security: + - jwtCookieAuth: [] + """ + if request.method == 'GET': + items = Item.query.all() + item_list = [user.to_dict() for user in items] + return jsonify({"success": True, "data": item_list}), 200 + +# /api/items +# POST # Új termék hozzáadása +@items.route('/', methods=['POST']) +@token_required(required_role=Roles.Admin.value) +def new_item(): + """ + Termék hozzáadása + --- + tags: + - Termékek + summary: Új termék hozzáadása (admin jogosultság szükséges). + description: Adminisztrátor új terméket vehet fel név, ár, mértékegység és ikon alapján. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [itemName, itemPrice, itemUnit, itemIconId] + properties: + itemName: + type: string + itemPrice: + type: integer + itemUnit: + type: string + itemIconId: + type: string + responses: + 200: + description: A termék sikeresen hozzáadva. + 400: + description: Hibás kérés (pl. hiányzó mezők vagy érvénytelen adatok). + security: + - jwtCookieAuth: [] + """ + if request.method == 'POST': + data = request.json + print(request) + + is_valid, msg = validate_json_fields(data, ['itemName', 'itemPrice', 'itemUnit', 'itemIconId'], ['itemPrice']) + if not is_valid: + return jsonify({"success": False, 'message': msg}), 400 + + # request tartalmának kiolvasása + item_name = str(data.get('itemName')) + item_price = int(str(data.get('itemPrice'))) + item_unit = str(data.get('itemUnit')) + item_icon_id = str(data.get('itemIconId')) + + # Ellenőrzés, hogy túl nagy árat adott-e meg a felhasználó + if item_price < 0 or item_price > TRANSACTION_AMOUNT_LIMIT: + return jsonify({"success": False, "message": f"Túl nagy vagy túl kis összeget adtál meg.\nLegalább 0 JMF, és legfeljebb {TRANSACTION_AMOUNT_LIMIT} JMF értékű terméket lehet felvenni!"}), 400 + + # Lekérdezzük az adatbázisból, hogy van e ilyen nevű termék már + item = Item.query.filter_by(name=item_name).first() + # Ha létezik már ilyen ilyen termék, akkor errort dobunk vissza + if item: + return jsonify({"success": False, "message": "Már létezik ilyen nevű termék!"}), 400 + + # Ikon kikeresése az adatbázisból + selected_icon = Icon.query.get(item_icon_id) + # Ha nem létezik ilyen ikon, akkor errort dobunk vissza + if not selected_icon: + return jsonify({"success": False, "message": ICON_DOES_NOT_EXISTS_MESSAGE}), 400 + + # Új Termék objektum létrehozása + new_item = Item( + name = item_name, + price = item_price, + unit = item_unit if item_unit != '' else DEFAULT_UNIT, + icon_id = selected_icon.id + ) + # Adatbázisba írás + db.session.add(new_item) + db.session.commit() + + return jsonify({"success": True, "message": "Tétel sikeresen hozzáadva!"}), 200 + +# /api/items/{item_id} +# GET # Termék adatainak lekérdezése az id-je alapján azonosítva +@items.route('/<string:item_id>', methods=['GET']) +def get_item(item_id): + """ + Termék adatainak lekérdezése + --- + tags: + - Termékek + summary: Egy adott termék adatainak lekérdezése. + description: A megadott azonosítóval rendelkező termék adatai. + parameters: + - name: item_id + in: path + required: true + description: A termék azonosítója. + schema: + type: integer + responses: + 200: + description: Sikeres lekérdezés. + 404: + description: Nincs ilyen termék. + security: + - jwtCookieAuth: [] + """ + if request.method == 'GET': + selected_item = Item.query.get(item_id) + + # Ha nem létezik ilyen termék, akkor errort dobunk vissza + if not selected_item: + return jsonify({"success": False, "message": ITEM_DOES_NOT_EXISTS_MESSAGE}), 400 + + return jsonify({"success": True, "data": selected_item.to_dict()}), 200 + +# /api/items/{item_id} +# PUT # Termék adatainak módosítása az id-je alapján azonosítva +@items.route('/<string:item_id>', methods=['PUT']) +@token_required(required_role=Roles.Admin.value) +def update_item(item_id): + """ + Termék adatainak módosítása + --- + tags: + - Termékek + summary: Egy adott termék adatainak módosítása. + description: A megadott azonosítóval rendelkező termék adatai módosíthatók. + parameters: + - name: item_id + in: path + required: true + description: A termék azonosítója. + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + price: + type: integer + unit: + type: string + icon_id: + type: string + archived: + type: boolean + responses: + 200: + description: Sikeres módosítás. + 404: + description: Nincs ilyen termék. + security: + - jwtCookieAuth: [] + """ + if request.method == 'PUT': + data = request.json + + # Lekérdezzük az adatbázisból, hogy van e ilyen nevű termék már + selected_item = Item.query.get(item_id) + + # Ha nem létezik még ilyen termék, akkor errort dobunk vissza + if not selected_item: + return jsonify({"success": False, "message": ITEM_DOES_NOT_EXISTS_MESSAGE}), 400 + + # request tartalmának kiolvasása, ha nem adtak meg értéket, akkor a már meglévő értékeket használjuk + item_name = str(data.get('name')) if 'name' in data else selected_item.name + item_price = int(str(data.get('price'))) if ('price' in data and is_numeric(data.get('price'))) else selected_item.price + item_unit = str(data.get('unit')) if 'unit' in data else selected_item.unit + icon_id = int(str(data.get('icon_id'))) if 'icon_id' in data and is_numeric(data.get('icon_id')) else selected_item.icon_id + archived = bool(data.get('archived')) if 'archived' in data else selected_icon.stock == ItemStates.ARCHIVED + + # Ellenőrzés, hogy túl nagy árat adott-e meg a felhasználó + if item_price < 0 or item_price > TRANSACTION_AMOUNT_LIMIT: + return jsonify({"success": False, "message": f"Túl nagy vagy túl kis összeget adtál meg.\nLegalább 0 JMF, és legfeljebb {TRANSACTION_AMOUNT_LIMIT} JMF értékű terméket lehet felvenni!"}), 400 + + # Ikon kikeresése az adatbázisból + selected_icon = Icon.query.get(icon_id) + + # Ha nem létezik ilyen ikon, akkor errort dobunk vissza + if not selected_icon: + return jsonify({"success": False, "message": ICON_DOES_NOT_EXISTS_MESSAGE}), 400 + + # Adatok módosítása + selected_item.name = item_name + selected_item.price = item_price + selected_item.unit = item_unit if item_unit != '' else DEFAULT_UNIT + selected_item.icon_id = icon_id + selected_item.stock = ItemStates.ARCHIVED if archived == True else ItemStates.INACTIVE + + # Adatbázisba írás + db.session.commit() + + return jsonify({"success": True, "message": f"{item_name} sikeresen módosítva!"}) + + +# /api/items/{item_id}/stock +# PUT # Termék készletének módosítása +@items.route('/<string:item_id>/stock', methods=['PUT']) +@token_required(required_role=Roles.Admin.value) +def update_item_stock(item_id): + """ + Termék készletének módosítása + --- + tags: + - Termékek + summary: Egy adott termék készletének módosítása. + description: A megadott azonosítóval rendelkező termék készlete módosítható. + parameters: + - name: item_id + in: path + required: true + description: A termék azonosítója. + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + stock: + type: integer + responses: + 200: + description: Sikeres módosítás. + 404: + description: Nincs ilyen termék. + security: + - jwtCookieAuth: [] + """ + if request.method == 'PUT': + data = request.json + + # Lekérdezzük az adatbázisból, hogy van e ilyen nevű termék már + selected_item = Item.query.get(item_id) + + # Ha nem létezik még ilyen termék, akkor errort dobunk vissza + if not selected_item: + return jsonify({"success": False, "message": ITEM_DOES_NOT_EXISTS_MESSAGE}), 400 + + # request tartalmának kiolvasása + item_stock = int(str(data.get('stock'))) if 'stock' in data and is_numeric(data.get('stock')) else selected_item.stock + + # Ellenőrzés + if item_stock in ItemStates.values(): + return jsonify({"success": False, "message": "Nincs ilyen készlet érték!"}), 400 + + # Adatok módosítása + selected_item.stock = item_stock + + # Adatbázisba írás + db.session.commit() + + return jsonify({"success": True, "message": f"{selected_item.name} készlete sikeresen módosítva!"}) diff --git a/app/api/users.py b/app/api/users.py index f2fcb9fe938d621633b3aed779b7df4c3c645d89..656581b21010e1893a7dfe11bf95fd291e1dfe74 100644 --- a/app/api/users.py +++ b/app/api/users.py @@ -1,5 +1,4 @@ -from flask import Blueprint, request, jsonify, Response, redirect, url_for - +from flask import Blueprint, request, jsonify from ..functions import validate_json_fields from ..token import get_current_user, token_required from ..database import db @@ -596,7 +595,7 @@ def get_user_role(uuid): # /api/users/{uuid}/role # PATCH # Megváltoztatja a felhasználó rangját @users.route('/<string:uuid>/role', methods=['PATCH']) -@token_required(required_role=Roles.admin.value) +@token_required(required_role=Roles.Admin.value) def update_user_role(uuid): """ Felhasználó rangjának módosítása UUID alapján diff --git a/app/app.py b/app/app.py index 086d9f1d926480168085b8cae62bdb4f0c8b1b34..c53abe0e37ea77947429eeda41a868b3de5aec35 100644 --- a/app/app.py +++ b/app/app.py @@ -97,7 +97,9 @@ app.register_blueprint(icon_requests, url_prefix='/') # ikon műveletek endpoint # === REST API endpointok === from .api.users import users +from .api.items import items app.register_blueprint(users, url_prefix= API_PREFIX + users.url_prefix) # oldalak endpoinjainak hozzáakpcsolása az apphoz +app.register_blueprint(items, url_prefix= API_PREFIX + items.url_prefix) # oldalak endpoinjainak hozzáakpcsolása az apphoz # === Adatbázis inizializálása === @@ -146,7 +148,7 @@ with app.app_context(): # Kontextus beállítása price=1, # unit=TOPUP_UNIT, icon_id=TOPUP_ICON_ID, - stock=INACTIVE_ITEM_STOCK + stock=ItemStates.INACTIVE.value ) # Adatbázisba írás db.session.add(topup_item) @@ -159,7 +161,7 @@ with app.app_context(): # Kontextus beállítása name=BUY_NAME, price=1, icon_id=BUY_ICON_ID, - stock=INACTIVE_ITEM_STOCK + stock=ItemStates.INACTIVE.value ) # Adatbázisba írás db.session.add(buy_item) diff --git a/app/config.py b/app/config.py index fca56faecb54e5be89c5b190032fb04421a61031..2660961f44d609d239986e436b5c14adbaab51a1 100644 --- a/app/config.py +++ b/app/config.py @@ -60,19 +60,6 @@ BUY_ICON_ID = DEFAULT_ICON_ID BUY_NAME = 'Vásárlás' # Termékek elérhetőségének kategorizálása a 'Item.stock' property által -SOLD_OUT_ITEM_STOCK = 0 -INFINITELY_AVALIBLE_ITEM_STOCK = -1 -INACTIVE_ITEM_STOCK = -2 -ARCHIVED_ITEM_STOCK = -3 - -# Termék készletállapotok -ITEM_STATES = { - 'sold_out': SOLD_OUT_ITEM_STOCK, - 'infinity': INFINITELY_AVALIBLE_ITEM_STOCK, - 'inactive': INACTIVE_ITEM_STOCK, - 'archived': ARCHIVED_ITEM_STOCK -} - class ItemStates(Enum): AVAILABLE = 1 SOLD_OUT = 0 @@ -83,3 +70,7 @@ class ItemStates(Enum): ONE_TIME = -4 ONE_TIME_PURCHASED = -5 + def __str__(self): + return self.name + def __int__(self): + return self.value diff --git a/app/functions.py b/app/functions.py index 701e82fe829dbb572fa7f760c607e7f49dc769fe..1670b25c1101576985756414c98452d7eb5ae3ba 100644 --- a/app/functions.py +++ b/app/functions.py @@ -23,3 +23,13 @@ def validate_json_fields(data: dict, required_fields: List[str], numeric_fields: return False, INVALID_FIELD_VALUES_MESSAGE return True, None + + +def is_numeric(value: str) -> bool: + """ + Check if the given value is numeric. + + :param value: The value to check (str) + :return: True if numeric, False otherwise + """ + return str(value).removeprefix('-').isdigit() \ No newline at end of file diff --git a/app/item_requests.py b/app/item_requests.py index b1dbcbd31c5f7598119953fffbad1f77dfd04e37..9d2df3520c35ca4a2c36970ca4ca21fda90b068e 100644 --- a/app/item_requests.py +++ b/app/item_requests.py @@ -228,7 +228,7 @@ def archive_item(): return jsonify({"success": False, "message": ITEM_DOES_NOT_EXISTS_MESSAGE}), 400 # Termék archiválása - selected_item.stock = ARCHIVED_ITEM_STOCK + selected_item.stock = ItemStates.ARCHIVED.value # Adatbázisba mentés db.session.commit() @@ -283,7 +283,7 @@ def dearchive_item(): return jsonify({"success": False, "message": ITEM_DOES_NOT_EXISTS_MESSAGE}), 400 # Termék archiválása - selected_item.stock = INACTIVE_ITEM_STOCK + selected_item.stock = ItemStates.INACTIVE.value # Adatbázisba mentés db.session.commit() @@ -354,7 +354,7 @@ def make_transaction(): return jsonify({"success": False, "message": "A vásárlás meghaladja az egyenleged!\nTölts fel pénzt BecskasszaSCH-ra!"}), 400 # A termék készletének csökkentése - if selected_item.stock != INFINITELY_AVALIBLE_ITEM_STOCK: + if selected_item.stock != ItemStates.INFINITY.value: # Többet venne, mint amennyi van készleten akkor hiba if selected_item.stock - quantity < 0: return jsonify({"success": False, "message": f"Nincs ennyi készleten a(z) {selected_item.name} termékből!"}), 400 @@ -622,7 +622,7 @@ def make_order(): return jsonify({"success": False, "message": f"A vásárlás meghaladja {selected_user.name} egyenlegét!\nTöltsön fel pénzt BecskasszaSCH-ra!"}), 400 # A termék készletének csökkentése - if selected_item.stock != INFINITELY_AVALIBLE_ITEM_STOCK: + if selected_item.stock != ItemStates.INFINITY.value: # Többet venne, mint amennyi van készleten akkor hiba if selected_item.stock - quantity < 0: return jsonify({"success": False, "message": f"Nincs ennyi készleten a(z) {selected_item.name} termékből!"}), 400 diff --git a/app/models.py b/app/models.py index 4cefce3a86cd5f885c1491cfe5d88f878b31fcf8..85b22dde2254eb1aaed5f748a7ee1f540a7b8a10 100644 --- a/app/models.py +++ b/app/models.py @@ -36,7 +36,7 @@ class Item(db.Model): name = db.Column(db.String(150), nullable=False) # Termék neve price = db.Column(db.Integer, nullable=False) # Termék ára unit = db.Column(db.String(10), nullable=False, default=DEFAULT_UNIT) # Termék mértékegysége - stock = db.Column(db.Integer, default=SOLD_OUT_ITEM_STOCK) # Termék készlete (alapból nincs készleten) + stock = db.Column(db.Integer, default=ItemStates.SOLD_OUT) # Termék készlete (alapból nincs készleten) icon_id = db.Column(db.Integer, db.ForeignKey('icons.id'), default=DEFAULT_ICON_ID) # Termék ikon-ra hivatkozó id-ja # Kapcsolt mező icon = db.relationship('Icon') # Referencia az ikonra (Adatbázisban nem jelenik meg) diff --git a/app/routes.py b/app/routes.py index 03c1f1045e94738011d3380f77d8261ceec2e7dd..0ead1ce3de5c79baa37e1f902a5f673996b45cd3 100644 --- a/app/routes.py +++ b/app/routes.py @@ -41,13 +41,13 @@ def home(): """ current_user = get_current_user() # A [Feltöltés] és a [Vásárlás] alap termékeken kívül jelenítse meg a többit - avalible_items = Item.query.filter(Item.id > NON_DISPLAY_BELLOW_ID).filter(Item.stock >= INFINITELY_AVALIBLE_ITEM_STOCK).all() + avalible_items = Item.query.filter(Item.id > NON_DISPLAY_BELLOW_ID).filter(Item.stock >= ItemStates.INFINITY.value).all() return render_template( "index.html", current_user=current_user, release_version=RELEASE_VERSION, items=sorted(avalible_items, key=lambda i: i.name), - item_states=ITEM_STATES + item_states=ItemStates, ) # Költési előzmények [ADMIN ONLY] @@ -265,9 +265,9 @@ def items(): current_user=current_user, release_version=RELEASE_VERSION, items=items_ordered, - archived_items = Item.query.filter(Item.stock == ARCHIVED_ITEM_STOCK).all(), + archived_items = Item.query.filter(Item.stock == ItemStates.ARCHIVED.value).all(), icons=Icon.query.filter(Icon.id > NON_DISPLAY_BELLOW_ID).all(), - item_states=ITEM_STATES + item_states=ItemStates ) @@ -345,7 +345,7 @@ def bar(): - jwtCookieAuth: [] """ current_user = get_current_user() - avalible_items = Item.query.filter(Item.id > NON_DISPLAY_BELLOW_ID).filter(Item.stock >= INFINITELY_AVALIBLE_ITEM_STOCK).all() + avalible_items = Item.query.filter(Item.id > NON_DISPLAY_BELLOW_ID).filter(Item.stock >= ItemStates.INFINITY.value).all() # A keresett felhasználó nevét kiolvassuk a GET kérésből search_field = request.args.get('search-field', '').lower() # Felhasználókra szűrés @@ -358,7 +358,7 @@ def bar(): release_version=RELEASE_VERSION, items=sorted(avalible_items, key=lambda i: i.name), users=sorted((users_results if search_field else users), key=lambda i: i.name), - item_states=ITEM_STATES + item_states=ItemStates ) @@ -547,11 +547,11 @@ def menu(): schema: type: string """ - avalible_items = Item.query.filter(Item.id > NON_DISPLAY_BELLOW_ID).filter(Item.stock >= INFINITELY_AVALIBLE_ITEM_STOCK).all() + avalible_items = Item.query.filter(Item.id > NON_DISPLAY_BELLOW_ID).filter(Item.stock >= ItemStates.INFINITY.value).all() return render_template( "menu.html", items=sorted(avalible_items, key=lambda i: i.name), - item_states=ITEM_STATES, + item_states=ItemStates, auto_refresh=True, display_only=True ) diff --git a/app/templates/bar.html b/app/templates/bar.html index 68cb3c1c6f47a5ffcafce6762f47d71532692b14..86ea4339e595c8841328f329c45d4bece7a70c62 100644 --- a/app/templates/bar.html +++ b/app/templates/bar.html @@ -90,10 +90,10 @@ <div class="d-block ms-2"> <h4 class="text-start">{{ item.name }}</h4> <h6 class="text-start">Ára: {{ item.price }} JMF / {{ item.unit }}</h6> - {% if item.stock == item_states.sold_out %} + {% if item.stock == item_states.SOLD_OUT.value %} <h6 class="text-start">Elfogyott</h6> {% else %} - <h6 class="text-start">Elérhető: {% if item.stock == item_states.infinity %} Bármennyi {% else %} {{ item.stock }} {{ item.unit }} {% endif %}</h6> + <h6 class="text-start">Elérhető: {% if item.stock == item_states.INFINITY.value %} Bármennyi {% else %} {{ item.stock }} {{ item.unit }} {% endif %}</h6> {% endif %} </div> <div class="d-none ms-auto align-items-center gap-2" id="remove{{ item.id }}"> diff --git a/app/templates/index.html b/app/templates/index.html index 0be3e0da87561824f279385b4134291477fa026c..c3d8336fd802fae32a33f3f488da18a02ec6637d 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -28,7 +28,7 @@ </div> <div class="d-block my-auto ms-2"> <h4 class="text-start">{{ item.name }}</h4> - {% if item.stock == item_states.sold_out %} + {% if item.stock == item_states.SOLD_OUT.value %} <h6 class="text-start">Elfogyott</h6> {% else %} <h6 class="text-start">Ára: {{ item.price }} JMF / {{ item.unit }}</h6> diff --git a/app/templates/items.html b/app/templates/items.html index d61ced72fd7a7fec931ed3d41bac32c718b3dd66..e4bbab5bec1c7169cbd3889e96c32130062744ff 100644 --- a/app/templates/items.html +++ b/app/templates/items.html @@ -70,11 +70,11 @@ <h5 class="text-center mb-0 text-nowrap"> {{ item.price }} JMF </h5> </div> <div class="d-none d-lg-block col-lg-2 d-flex align-items-center"> - {% if item.stock >= item_states.sold_out %} + {% if item.stock >= item_states.SOLD_OUT.value %} <h5 class="text-center mb-0 text-nowrap"> {{ item.stock }} {{ item.unit }} </h5> - {% endif %} {% if item.stock == item_states.infinity %} + {% endif %} {% if item.stock == item_states.INFINITY.value %} <h5 class="text-center mb-0 text-nowrap"> bármennyi </h5> - {% endif %} {% if item.stock == item_states.inactive %} + {% endif %} {% if item.stock == item_states.INACTIVE.value %} <h5 class="text-center mb-0 text-nowrap"> inaktív </h5> {% endif %} </div> @@ -82,9 +82,9 @@ <div class="ms-auto d-block"> <h5 class="text-end mb-0 text-nowrap"> {{ item.price }} JMF </h5> <h5 class="text-end mb-0 text-nowrap"> - {% if item.stock >= item_states.sold_out %} {{ item.stock }} {{ item.unit }} {% endif %} - {% if item.stock == item_states.infinity %} bármennyi {% endif %} - {% if item.stock == item_states.inactive %} inaktív {% endif %} + {% if item.stock >= item_states.SOLD_OUT.value %} {{ item.stock }} {{ item.unit }} {% endif %} + {% if item.stock == item_states.INFINITY.value %} bármennyi {% endif %} + {% if item.stock == item_states.INACTIVE.value %} inaktív {% endif %} </h5> </div> <div class="d-block"> diff --git a/app/templates/menu.html b/app/templates/menu.html index 50693826693aa173a929cdba007789ce97bd4676..3c8d83e14b1f568c79192601021b89e992cb1ec4 100644 --- a/app/templates/menu.html +++ b/app/templates/menu.html @@ -19,7 +19,7 @@ </div> <div class="d-block my-auto ms-2"> <h4 class="text-start">{{ item.name }}</h4> - {% if item.stock == item_states.sold_out %} + {% if item.stock == item_states.SOLD_OUT.value %} <h6 class="text-start">Elfogyott</h6> {% else %} <h6 class="text-start">Ára: {{ item.price }} JMF / {{ item.unit }}</h6>