diff --git a/.gitignore b/.gitignore index 2820fad..1d1cfe8 100644 --- a/.gitignore +++ b/.gitignore @@ -339,6 +339,7 @@ pip-selfcheck.json # Misc config.yaml scans.json +test_*.* test.* *.db .vscode @@ -348,4 +349,5 @@ test.* .flaskenv* !.env.project !.env.vault -*.ps1 \ No newline at end of file +*.ps1 +*.pdf \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 135ac14..76524df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,14 +5,13 @@ RUN useradd costhive WORKDIR /home/costhive RUN apt update && apt -y upgrade; \ - apt install -y libpq-dev gcc g++ swig make; \ + apt install -y libpq-dev gcc g++ swig make cmake m4; \ rm -rf /var/lib/apt/lists -COPY boot.sh requirements.txt ./ +COPY boot.sh backend/requirements.txt ./ RUN python -m venv venv; \ venv/bin/pip install --upgrade pip; \ - venv/bin/pip install wheel; \ - venv/bin/pip install gunicorn; \ + venv/bin/pip install wheel gunicorn; \ venv/bin/pip install -r requirements.txt COPY backend backend @@ -20,7 +19,7 @@ COPY backend backend ENV FLASK_APP run.py RUN chmod +x boot.sh; \ - chown -R costhive:costhive ./ + chown -R costhive:costhive . USER costhive diff --git a/backend/Pipfile b/backend/Pipfile index 427a791..a84b1dc 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -21,6 +21,7 @@ python-dotenv = "*" pymupdf = "*" requests = "*" sqlalchemy-utils = "*" +wtforms-sqlalchemy = "*" [dev-packages] diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index 10319cb..aed324c 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "6aa4fc89c41e2a25283179a13d468214fdebcbd824a74a7f2f79482b920eed78" + "sha256": "5418d6b50788a542461e9a348ad0864d3e4d2046e48967df081cbaed9971ed0f" }, "pipfile-spec": 6, "requires": { @@ -25,14 +25,6 @@ "markers": "python_version >= '3.7'", "version": "==1.11.1" }, - "anyio": { - "hashes": [ - "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780", - "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5" - ], - "markers": "python_version >= '3.7'", - "version": "==3.7.1" - }, "blinker": { "hashes": [ "sha256:4afd3de66ef3a9f8067559fb7a1cbe555c17dcbe15971b05d1b625c3e7abe213", @@ -43,11 +35,11 @@ }, "certifi": { "hashes": [ - "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7", - "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716" + "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082", + "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9" ], "markers": "python_version >= '3.6'", - "version": "==2023.5.7" + "version": "==2023.7.22" }, "charset-normalizer": { "hashes": [ @@ -132,11 +124,11 @@ }, "click": { "hashes": [ - "sha256:4be4b1af8d665c6d942909916d31a213a106800c47d0eeba73d34da3cbc11367", - "sha256:e576aa487d679441d7d30abb87e1b43d24fc53bffb8758443b1a9e1cee504548" + "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd", + "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5" ], "markers": "python_version >= '3.7'", - "version": "==8.1.5" + "version": "==8.1.6" }, "colorama": { "hashes": [ @@ -148,11 +140,11 @@ }, "dnspython": { "hashes": [ - "sha256:46b4052a55b56beea3a3bdd7b30295c292bd6827dd442348bc116f2d35b17f0a", - "sha256:758e691dbb454d5ccf4e1b154a19e52847f79e21a42fef17b969144af29a4e6c" + "sha256:5b7488477388b8c0b70a8ce93b227c5603bc7b77f1565afe8e729c36c51447d7", + "sha256:c33971c79af5be968bb897e95c2448e11a645ee84d93b265ce0b7aabe5dfdca8" ], "markers": "python_version >= '3.8' and python_version < '4.0'", - "version": "==2.4.0" + "version": "==2.4.1" }, "dominate": { "hashes": [ @@ -170,14 +162,6 @@ "index": "pypi", "version": "==2.0.0.post2" }, - "exceptiongroup": { - "hashes": [ - "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5", - "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f" - ], - "markers": "python_version < '3.11'", - "version": "==1.1.2" - }, "flask": { "hashes": [ "sha256:77fd4e1249d8c9923de34907236b747ced06e5467ecac1a7bb7115ae0e9670b0", @@ -314,22 +298,6 @@ "markers": "platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))", "version": "==2.0.2" }, - "h11": { - "hashes": [ - "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", - "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761" - ], - "markers": "python_version >= '3.7'", - "version": "==0.14.0" - }, - "httpcore": { - "hashes": [ - "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888", - "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87" - ], - "markers": "python_version >= '3.8'", - "version": "==0.17.3" - }, "idna": { "hashes": [ "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", @@ -420,11 +388,11 @@ }, "marshmallow": { "hashes": [ - "sha256:90032c0fd650ce94b6ec6dc8dfeb0e3ff50c144586462c389b81a07205bedb78", - "sha256:93f0958568da045b0021ec6aeb7ac37c81bfcccbb9a0e7ed8559885070b3a19b" + "sha256:5d2371bbe42000f2b3fb5eaa065224df7d8f8597bc19a1bbfa5bfe7fba8da889", + "sha256:684939db93e80ad3561392f47be0230743131560a41c5110684c16e21ade0a5c" ], - "markers": "python_version >= '3.7'", - "version": "==3.19.0" + "markers": "python_version >= '3.8'", + "version": "==3.20.1" }, "marshmallow-sqlalchemy": { "hashes": [ @@ -512,11 +480,11 @@ }, "pyjwt": { "hashes": [ - "sha256:ba2b425b15ad5ef12f200dc67dd56af4e26de2331f965c5439994dad075876e1", - "sha256:bd6ca4a3c4285c1a2d4349e5a035fdf8fb94e04ccd0fcbe6ba289dae9cc3e074" + "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de", + "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320" ], "index": "pypi", - "version": "==2.7.0" + "version": "==2.8.0" }, "pymupdf": { "hashes": [ @@ -570,14 +538,6 @@ "index": "pypi", "version": "==2.31.0" }, - "sniffio": { - "hashes": [ - "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", - "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384" - ], - "markers": "python_version >= '3.7'", - "version": "==1.3.0" - }, "sqlalchemy": { "hashes": [ "sha256:024d2f67fb3ec697555e48caeb7147cfe2c08065a4f1a52d93c3d44fc8e6ad1c", @@ -643,11 +603,11 @@ }, "urllib3": { "hashes": [ - "sha256:48e7fafa40319d358848e1bc6809b208340fafe2096f1725d05d67443d0483d1", - "sha256:bee28b5e56addb8226c96f7f13ac28cb4c301dd5ea8a6ca179c0b9835e032825" + "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11", + "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4" ], "markers": "python_version >= '3.7'", - "version": "==2.0.3" + "version": "==2.0.4" }, "visitor": { "hashes": [ @@ -670,6 +630,14 @@ ], "markers": "python_version >= '3.7'", "version": "==3.0.1" + }, + "wtforms-sqlalchemy": { + "hashes": [ + "sha256:7ca42824ad7c453a036f502b42d9c17d4ad8398ff9248a62ed538d1ce0bc41c3", + "sha256:90195d7592bf256d82498c42c79d416832e4a4e6fbca4f1e745a018f66d26c47" + ], + "index": "pypi", + "version": "==0.3" } }, "develop": {} diff --git a/backend/migrations/versions/0fa2ef37e440_receipt_id_serial.py b/backend/migrations/versions/0fa2ef37e440_receipt_id_serial.py new file mode 100644 index 0000000..2b967a7 --- /dev/null +++ b/backend/migrations/versions/0fa2ef37e440_receipt_id_serial.py @@ -0,0 +1,54 @@ +"""receipt id serial + +Revision ID: 0fa2ef37e440 +Revises: f6f97ed9c053 +Create Date: 2023-07-25 21:26:25.353435 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '0fa2ef37e440' +down_revision = 'f6f97ed9c053' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.execute("CREATE SEQUENCE receipt_id_seq;") + + with op.batch_alter_table('receipt', schema=None) as batch_op: + batch_op.alter_column('id', + existing_type=sa.BIGINT(), + type_=sa.Integer(), + existing_nullable=False, + server_default=sa.sql.func.next_value(sa.Sequence('receipt_id_seq'))) + + with op.batch_alter_table('receipt_item', schema=None) as batch_op: + batch_op.alter_column('receipt', + existing_type=sa.BIGINT(), + type_=sa.Integer(), + existing_nullable=False) + + op.execute("ALTER SEQUENCE receipt_id_seq OWNED BY receipt.id;") + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('receipt_item', schema=None) as batch_op: + batch_op.alter_column('receipt', + existing_type=sa.Integer(), + type_=sa.BIGINT(), + existing_nullable=False) + + with op.batch_alter_table('receipt', schema=None) as batch_op: + batch_op.alter_column('id', + existing_type=sa.Integer(), + type_=sa.BIGINT(), + existing_nullable=False) + + # ### end Alembic commands ### diff --git a/backend/models/brand.py b/backend/models/brand.py index 59080eb..576d275 100644 --- a/backend/models/brand.py +++ b/backend/models/brand.py @@ -9,3 +9,6 @@ class Brand(db.Model): def __repr__(self) -> str: return f"" + + def __str__(self) -> str: + return f"{self.name}" diff --git a/backend/models/item.py b/backend/models/item.py index 87f9b39..52d8b06 100644 --- a/backend/models/item.py +++ b/backend/models/item.py @@ -21,3 +21,6 @@ class Item(db.Model): def __repr__(self) -> str: return f"" + + def __str__(self) -> str: + return f"({self.id}) {self.description}" diff --git a/backend/src/receipts/__init__.py b/backend/src/receipts/__init__.py index dff31d0..fbe0cce 100644 --- a/backend/src/receipts/__init__.py +++ b/backend/src/receipts/__init__.py @@ -1,5 +1,7 @@ from flask import Blueprint bp = Blueprint('receipts', __name__, url_prefix='/receipts') - -from . import forms, routes \ No newline at end of file +from .upload import bp as bp_upload +bp.register_blueprint(bp_upload) +from .check_items import bp as bp_check_items +bp.register_blueprint(bp_check_items) \ No newline at end of file diff --git a/backend/src/receipts/check_items/__init__.py b/backend/src/receipts/check_items/__init__.py new file mode 100644 index 0000000..78a7204 --- /dev/null +++ b/backend/src/receipts/check_items/__init__.py @@ -0,0 +1,5 @@ +from flask import Blueprint + +bp = Blueprint('check_items', __name__, url_prefix='/check_items') + +from . import forms, routes \ No newline at end of file diff --git a/backend/src/receipts/check_items/forms.py b/backend/src/receipts/check_items/forms.py new file mode 100644 index 0000000..c135330 --- /dev/null +++ b/backend/src/receipts/check_items/forms.py @@ -0,0 +1,63 @@ +from collections import namedtuple +from flask_wtf import FlaskForm +from models import Brand, Item +from src.utils.models.query_factories import all_brands, all_items +from wtforms import BooleanField, HiddenField, FieldList, Form, FormField, IntegerField, RadioField, SelectField, StringField, SubmitField +from wtforms.validators import DataRequired, Optional, ValidationError +from wtforms_sqlalchemy.fields import QuerySelectField + + +class CheckItemsEntryForm(Form): + # TODO Fertig machen x.x + itemname = HiddenField('itemname', validators=[DataRequired()]) + price = HiddenField('price', validators=[DataRequired()]) + requesting = BooleanField("", default=False, render_kw={ + "class": "form-check-input"}) + new_or_existing = RadioField("", choices=[ + (0, "New"), (1, "Existing")], render_kw={"class": "form-check-input", "style": "display:none"}, validate_choice=False) + # Fields for new Item + new_description = StringField("Description", render_kw={"class": "form-control"}) + new_amount_change = IntegerField("Amount", render_kw={"class": "form-control"}) + new_brand = QuerySelectField("Brand", query_factory=all_brands, render_kw={"class": "form-control"}, allow_blank=True) + # Fields for existing Item + existing_item = QuerySelectField("Item", query_factory=all_items, render_kw={"class": "form-control"}, allow_blank=True) + + def validate_new_or_existing(self, new_or_existing): + if (self.requesting.data and not new_or_existing.data): + raise ValidationError( + "Please choose if it's a new or existing Item.") + + def validate_existing_item(self, existing_item): + if (existing_item.data and self.new_or_existing.data == 0): + raise ValidationError("You shouldn't be able to enter this.") + + def validate_new_description(self, new_description): + if (new_description.data and self.new_or_existing.data == 1): + raise ValidationError("You shouldn't be able to enter this.") + + def validate_new_amount_change(self, new_amount_change): + if (new_amount_change.data and self.new_or_existing.data == 1): + raise ValidationError("You shouldn't be able to enter this.") + + def validate_new_brand(self, new_brand): + if (new_brand.data and self.new_or_existing.data == 1): + raise ValidationError("You shouldn't be able to enter this.") + + +class CheckItemsForm(FlaskForm): + items = FieldList(FormField(CheckItemsEntryForm)) + submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"}) + + @classmethod + def new(cls, itemarray): + CheckItemsEntry = namedtuple( + "CheckItemsEntry", ["itemname", "price", "new_brand"]) + CheckItems = namedtuple("CheckItems", ["items"]) + check_items_entry = [] + for item in itemarray: + check_items_entry.append(CheckItemsEntry(item['itemname'], item['price'], 0)) + check_items = CheckItems(check_items_entry) + form = cls(obj=check_items) + + print(f"{form.items.entries}") + return form diff --git a/backend/src/receipts/check_items/routes.py b/backend/src/receipts/check_items/routes.py new file mode 100644 index 0000000..2fd4653 --- /dev/null +++ b/backend/src/receipts/check_items/routes.py @@ -0,0 +1,41 @@ +from flask import abort, request, url_for +from flask_login import current_user, login_required +from . import bp +from .forms import CheckItemsForm +from src import db, LOGGER +from models.receipt import Receipt +from models.login_token import LoginToken +from src.utils.pdf_receipt_parser import PDFReceipt +from src.utils.routes_utils import render_custom_template as render_template + +PDFDir = "./" + +@bp.route('/', methods=['GET', 'POST']) +@login_required +def confirm_receipt_items(receipt_id: int): + """Check items from a receipt if they should be accounted for payment. + Get those items from the receipt PDF itself.""" + receipt_details = Receipt.query.get(receipt_id) + if current_user.is_authenticated and current_user.id == receipt_details.LoginToken.Establishment.owner: + receipt = PDFReceipt.getPDFReceiptFromFile(PDFDir + f"{receipt_details.id}.pdf") + form = CheckItemsForm.new(receipt.items) + # TODO: Precheck if items are already in database. If yes, check if item is present only once or multiple + # times and provide dropdown menu if necessary. If not, provide input field. + # temp_choices = [] + # for item in receipt.items: + # match item: + # case {"itemname": itemname, "price": price}: + # temp_choices.append((itemname.replace(" ", "_"), f"{itemname, price}")) + # case {"itemname": itemname, "price": price, "amount": amount}: + # temp_choices.append((itemname.replace(" ", "_"), f"{itemname}, {price} * {amount}")) + # form.choices = temp_choices + # print(form.data) + for formitem in form.items: + # print(formitem.new_brand.__dict__) + print(formitem.data) + if form.validate(): + print("valid") + if form.validate_on_submit(): + return form.items.data + return render_template("receipts/check_items.html", form=form) + abort(403) \ No newline at end of file diff --git a/backend/src/receipts/forms.py b/backend/src/receipts/forms.py deleted file mode 100644 index d3119e0..0000000 --- a/backend/src/receipts/forms.py +++ /dev/null @@ -1,24 +0,0 @@ -from flask_wtf import FlaskForm -from flask_wtf.file import FileAllowed, FileField, FileRequired -from wtforms import BooleanField, SelectMultipleField, SubmitField, widgets -from wtforms.validators import Optional - -class MultiCheckboxField(SelectMultipleField): - widget = widgets.ListWidget(prefix_label=False) - option_widget = widgets.CheckboxInput() - -class UploadReceiptForm(FlaskForm): - pdfReceipt = FileField("PDF", validators=[FileRequired(), FileAllowed(["pdf"], "Invalid Format, must be .pdf")]) - submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"}) - -class CheckItemsForm(FlaskForm): - items = MultiCheckboxField("Items") - submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"}) - - #TODO create new() method which loads the form for a specific receipt - @classmethod - def new(cls, itemArray): - """ - """ - form = cls() - return form \ No newline at end of file diff --git a/backend/src/receipts/routes.py b/backend/src/receipts/routes.py deleted file mode 100644 index 7e6742b..0000000 --- a/backend/src/receipts/routes.py +++ /dev/null @@ -1,35 +0,0 @@ -from flask import abort, request, url_for -from flask_login import current_user, login_required -from werkzeug.utils import secure_filename -from . import bp -from .forms import UploadReceiptForm -from src import db, LOGGER -from models.receipt import Receipt -from models.login_token import LoginToken -from src.utils.pdf_receipt_parser import PDFReceipt -from src.utils.routes_utils import render_custom_template as render_template - -PDFDir = "./" - -@bp.route('/upload_receipt', methods=['GET', 'POST']) -@login_required -def upload_receipt(): - """Upload of a receipt.""" - if current_user.is_anonymous: - abort(403) - if "establishment" in request.args: - if LoginToken.query.filter_by(establishment=request.args['establishment'], user=current_user.id).first(): - form = UploadReceiptForm() - LOGGER.debug(form.pdfReceipt.data) - if form.validate_on_submit(): - receipt = PDFReceipt(form.pdfReceipt.data) - dbReceipt = Receipt(id = receipt.id, date = receipt.date, - from_user = LoginToken.query.filter_by(establishment=request.args['establishment'], user=current_user.id).first().token) - form.pdfReceipt.data.save(PDFDir + secure_filename(f"{str(receipt.date)}_{receipt.id}.pdf")) - db.session.add(dbReceipt) - db.session.commit() - return receipt.text.replace("\n", "
") - else: - LOGGER.debug(form.errors) - return render_template("receipts/upload.html", form = form) - abort(403) \ No newline at end of file diff --git a/backend/src/receipts/upload/routes.py b/backend/src/receipts/upload/routes.py index aca1cf2..00b8499 100644 --- a/backend/src/receipts/upload/routes.py +++ b/backend/src/receipts/upload/routes.py @@ -1,5 +1,6 @@ from flask import abort, request, url_for from flask_login import current_user, login_required +from os import rename from werkzeug.utils import secure_filename from . import bp from .forms import UploadReceiptForm @@ -11,7 +12,7 @@ from src.utils.routes_utils import render_custom_template as render_template PDFDir = "./" # TODO überarbeiten. PDFs müssen in der Datenbank eine eigene ID bekommen. -# Quittungen haben eine USt.-ID. Die muss als Unique Key in der Datenbank +# Quittungen haben eine gesonderte ID. Die muss als Unique Key in der Datenbank # hinterlegt sein. # Die laufende ID ist zum abspeichern der PDFs gedacht. @bp.route('/', methods=['GET', 'POST']) @@ -20,16 +21,19 @@ def upload_receipt(establishment: int): """Upload of a receipt.""" if current_user.is_anonymous: abort(403) - if LoginToken.query.filter_by(establishment=request.args['establishment'], user=current_user.id).first(): + if LoginToken.query.filter_by(establishment=establishment, user=current_user.id).first(): form = UploadReceiptForm() LOGGER.debug(form.pdfReceipt.data) if form.validate_on_submit(): - receipt = PDFReceipt(form.pdfReceipt.data) - dbReceipt = Receipt(id = receipt.id, date = receipt.date, - from_user = LoginToken.query.filter_by(establishment=request.args['establishment'], user=current_user.id).first().token) - form.pdfReceipt.data.save(PDFDir + secure_filename(f"{str(receipt.date)}_{receipt.id}.pdf")) + pdfReceipt = form.pdfReceipt.data + pdfReceipt.save(f"{PDFDir}/temp.pdf") + with open(f"{PDFDir}/temp.pdf") as doc: + receipt = PDFReceipt(doc) + dbReceipt = Receipt(bonid = receipt.id, date = receipt.date, + from_user = LoginToken.query.filter_by(establishment=establishment, user=current_user.id).first().token) db.session.add(dbReceipt) db.session.commit() + rename(f"{PDFDir}/temp.pdf", f"{PDFDir}{secure_filename(f'{dbReceipt.id}.pdf')}") return receipt.text.replace("\n", "
") else: LOGGER.debug(form.errors) diff --git a/backend/src/utils/models/query_factories.py b/backend/src/utils/models/query_factories.py new file mode 100644 index 0000000..e29a7ba --- /dev/null +++ b/backend/src/utils/models/query_factories.py @@ -0,0 +1,10 @@ +from models import Brand, Category, Item + +def all_brands(): + return Brand.query.order_by("name") + +def all_categories(): + return Category.query.order_by("name") + +def all_items(): + return Item.query.order_by("id") \ No newline at end of file diff --git a/backend/web/templates/base.html b/backend/web/templates/base.html index d5f50ae..6af05b4 100644 --- a/backend/web/templates/base.html +++ b/backend/web/templates/base.html @@ -122,5 +122,5 @@ CostHive - + {% endblock %} \ No newline at end of file diff --git a/backend/web/templates/receipts/check_items.html b/backend/web/templates/receipts/check_items.html new file mode 100644 index 0000000..d137543 --- /dev/null +++ b/backend/web/templates/receipts/check_items.html @@ -0,0 +1,67 @@ +{% extends "base.html" %} +{% import 'bootstrap/wtf.html' as wtf %} +{% from 'utils/form/_render_field.html' import render_field %} + +{% block app_content %} +
+ {{ form.hidden_tag() }} + {% for item in form.items %} +

{{ item.requesting() }} {{ item.data.itemname }} (€{{ item.data.price }})

+ {{ render_field(item.itemname) }} + {{ render_field(item.price) }} + + + + +
+ {% endfor %} + {{ form.submit() }} +
+{% endblock %} + + +{% block scripts %} + {{ super() }} + {% for item in form.items %} + + {% endfor %} +{% endblock %} \ No newline at end of file diff --git a/backend/web/templates/utils/form/_render_field.html b/backend/web/templates/utils/form/_render_field.html index 8532f52..f3b91a4 100644 --- a/backend/web/templates/utils/form/_render_field.html +++ b/backend/web/templates/utils/form/_render_field.html @@ -1,11 +1,13 @@ +{% from 'utils/form/_render_radio_button.html' import render_field as render_radio %} +{% from 'utils/form/_render_generic_field.html' import render_field as render_generic %} +{% from 'utils/form/_render_hidden_field.html' import render_field as render_hidden %} + {% macro render_field(field) %} -
- {{ field.label }} {{ field(**kwargs)|safe }} {% if field.errors %} -
    - {% for error in field.errors %} -
  • {{ error }}
  • - {% endfor %} -
- {% endif %} -
-{% endmacro %} +{% if field.__class__.__name__ == "RadioField" %} +{{ render_radio(field) }} +{% elif field.__class__.__name__ == "HiddenField" %} +{{ render_hidden(field) }} +{% else %} +{{ render_generic(field)}} +{% endif %} +{% endmacro %} \ No newline at end of file diff --git a/backend/web/templates/utils/form/_render_fieldlist.html b/backend/web/templates/utils/form/_render_fieldlist.html new file mode 100644 index 0000000..e7b9c11 --- /dev/null +++ b/backend/web/templates/utils/form/_render_fieldlist.html @@ -0,0 +1,11 @@ +{% macro render_fieldlist(field) %} +
+ {{ field.label }} {{ field(**kwargs)|safe }} {% if field.errors %} +
    + {% for error in field.errors %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} +
+{% endmacro %} \ No newline at end of file diff --git a/backend/web/templates/utils/form/_render_generic_field.html b/backend/web/templates/utils/form/_render_generic_field.html new file mode 100644 index 0000000..be5409e --- /dev/null +++ b/backend/web/templates/utils/form/_render_generic_field.html @@ -0,0 +1,11 @@ +{% macro render_field(field) %} +
+ {{ field.label }} {{ field(**kwargs)|safe }} {% if field.errors %} +
    + {% for error in field.errors %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} +
+{% endmacro %} \ No newline at end of file diff --git a/backend/web/templates/utils/form/_render_hidden_field.html b/backend/web/templates/utils/form/_render_hidden_field.html new file mode 100644 index 0000000..0a55c0c --- /dev/null +++ b/backend/web/templates/utils/form/_render_hidden_field.html @@ -0,0 +1,11 @@ +{% macro render_field(field) %} +
+ {{ field(**kwargs)|safe }} {% if field.errors %} +
    + {% for error in field.errors %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} +
+{% endmacro %} \ No newline at end of file diff --git a/backend/web/templates/utils/form/_render_radio_button.html b/backend/web/templates/utils/form/_render_radio_button.html new file mode 100644 index 0000000..7f58bac --- /dev/null +++ b/backend/web/templates/utils/form/_render_radio_button.html @@ -0,0 +1,16 @@ +{% macro render_field(field) %} +{% for choice in field.choices %} +
+ + +
+{% endfor %} +{% if field.errors %} +
    + {% for error in field.errors %} +
  • {{ error }}
  • + {% endfor %} +
+{% endif %} +{% endmacro %} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 34875a7..da5115f 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/requirements.txt.backup b/requirements.txt.backup new file mode 100644 index 0000000..34875a7 Binary files /dev/null and b/requirements.txt.backup differ