diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..20d3a04 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 3 + + - package-ecosystem: "pip" + directory: "/kaaf" + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/workflows/deploy-refusjon.yml b/.github/workflows/deploy-refusjon.yml index 47713fd..a3c420a 100644 --- a/.github/workflows/deploy-refusjon.yml +++ b/.github/workflows/deploy-refusjon.yml @@ -2,27 +2,27 @@ name: Deploy refusjon container to Azure on: push: - branches: [ refusjon-master ] + branches: refusjon-master jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: azure/docker-login@v1 with: - login-server: ntnuiskjema.azurecr.io + login-server: ntnuiservices.azurecr.io username: ${{ secrets.REGISTRY_USERNAME }} password: ${{ secrets.REGISTRY_PASSWORD }} - run: | - docker build . -t ntnuiskjema.azurecr.io/skjema-refusjon:latest - docker push ntnuiskjema.azurecr.io/skjema-refusjon:latest + docker build . -t ntnuiservices.azurecr.io/skjema-refusjon:latest + docker push ntnuiservices.azurecr.io/skjema-refusjon:latest - uses: azure/webapps-deploy@v2 with: - app-name: 'ntnuirefusjon' + app-name: 'ntnui-skjema-refusjon' publish-profile: ${{ secrets.REFUSJON_AZURE_WEBAPP_PUBLISH_PROFILE }} - images: 'ntnuiskjema.azurecr.io/skjema-refusjon:latest' \ No newline at end of file + images: 'ntnuiservices.azurecr.io/skjema-refusjon:latest' \ No newline at end of file diff --git a/.github/workflows/deploy-reise.yml b/.github/workflows/deploy-reise.yml index 8ef82ab..20a85c7 100644 --- a/.github/workflows/deploy-reise.yml +++ b/.github/workflows/deploy-reise.yml @@ -2,27 +2,27 @@ name: Deploy reise container to Azure on: push: - branches: [ reise-master ] + branches: reise-master jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: azure/docker-login@v1 with: - login-server: ntnuiskjema.azurecr.io + login-server: ntnuiservices.azurecr.io username: ${{ secrets.REGISTRY_USERNAME }} password: ${{ secrets.REGISTRY_PASSWORD }} - run: | - docker build . -t ntnuiskjema.azurecr.io/skjema-reise:latest - docker push ntnuiskjema.azurecr.io/skjema-reise:latest + docker build . -t ntnuiservices.azurecr.io/skjema-reise:latest + docker push ntnuiservices.azurecr.io/skjema-reise:latest - uses: azure/webapps-deploy@v2 with: - app-name: 'ntnuireise' + app-name: 'ntnui-skjema-reise' publish-profile: ${{ secrets.REISE_AZURE_WEBAPP_PUBLISH_PROFILE }} - images: 'ntnuiskjema.azurecr.io/skjema-reise:latest' \ No newline at end of file + images: 'ntnuiservices.azurecr.io/skjema-reise:latest' \ No newline at end of file diff --git a/.gitignore b/.gitignore index b392aab..a89c9f3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ webapp/.next/ webapp/out/ venv/ kaaf/__pycache__/ -.vscode \ No newline at end of file +.vscode +.env +output.pdf \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 506e471..cfe3d64 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,5 @@ -FROM openfaas/of-watchdog:0.8.2 as watchdog -FROM python:3.7-slim AS build-backend - -RUN apt-get update && apt-get install -y poppler-utils +FROM ghcr.io/openfaas/of-watchdog:0.9.11 as watchdog +FROM python:3.11 AS build-backend WORKDIR /app @@ -10,6 +8,8 @@ RUN chmod +x /usr/bin/fwatchdog COPY ./kaaf/req.txt ./kaaf/req.txt +RUN python -m pip install --upgrade pip + RUN pip install --no-cache-dir -r kaaf/req.txt FROM node:16-alpine3.11 AS build-frontend diff --git a/README.md b/README.md index 7473aa9..4909757 100644 --- a/README.md +++ b/README.md @@ -17,29 +17,29 @@ To run just the frontend: To run the backend/everything: - Make a virtual env with `python -m venv venv` -- Enter the env with `source venv/bin/activate` +- Enter the env with (Unix) `source venv/bin/activate` or (Windows) `source venv/Scripts/activate` +- Make sure you are using latest pip with `python -m pip install --upgrade pip` - Install packages with `pip install -r kaaf/req.txt` - Start the server with `python kaaf/server.py` - If the frontend is exported (`yarn export`), the webapp will be available at `localhost:5000` when running `server.py` -> One of the packages (pdf2image) will require poppler to work correctly with tmp files. Most linux distros come with this. -> For MacOS `brew install poppler` - ### Generating PDFs It might be nice to be able to quickly generate PDFs when developing, without having to start up everything. To do this you can run: ```python -python kaaf/generate-example.py signature.png output.pdf image0.png image1.png ... +python kaaf/generate-example.py signature.png attachment1.png attachment2.pdf ... ``` -Where `signature.png` and `imageN.png` are paths to image files (the latter images are optional) +Where `signature.png` and `attachmentN.XYZ` are paths to image files. ## Environment variables +While developing locally, you can temporarily add environment variables to the Dockerfile, such as `ENV MAIL_ADDRESS="no-reply@ntnui.no"`, or by creating an **.env** file with `KEY=VALUE` pairs separated by a newline. + | Variable | Function | | --------------- | -------------------------------------------- | -| `MAIL_ADDRESS` | Set the mail address for generated receipts | -| `MAIL_PASSWORD` | Password for the mail account | +| `SERVICE_ACCOUNT_STR` | Google service account string | +| `MAIL_ADDRESS` | Set the mail address for generated receipts | | `ENVIRONMENT` | Set to "production" for sentry errors | | `SENTRY_DSN` | Ingest errors to sentry | diff --git a/images/16bit-depth.png b/images/16bit-depth.png new file mode 100644 index 0000000..c7ef124 Binary files /dev/null and b/images/16bit-depth.png differ diff --git a/images/dragvoll.HEIC b/images/dragvoll.HEIC new file mode 100644 index 0000000..f219f2b Binary files /dev/null and b/images/dragvoll.HEIC differ diff --git a/images/example-old-output.pdf b/images/example-old-output.pdf new file mode 100644 index 0000000..8787234 Binary files /dev/null and b/images/example-old-output.pdf differ diff --git a/images/example-signature.png b/images/example-signature.png new file mode 100644 index 0000000..54df77b Binary files /dev/null and b/images/example-signature.png differ diff --git a/kaaf/generate-example.py b/kaaf/generate-example.py index 47e0ec4..1b4205d 100644 --- a/kaaf/generate-example.py +++ b/kaaf/generate-example.py @@ -1,49 +1,78 @@ import argparse import base64 +import sys +import os +import magic -from handler import create_pdf, modify_data +from handler import create_pdf -default_data = { - "date": "2020-12-27", - "amount": "69 kr", - "name": "Mats", - "accountNumber": "010101010101", - "committee": "Hovedstyret", - "occasion": "Teste litt", - "comment": "pls work", +test_data = { + "name": "John Doe", + "mailfrom": "johndoe@ntnui.dev", + "committee": "Sprint", + "accountNumber": "123456789", + "amount": "69.69", + "date": "2023-05-17", + "occasion": "Expense reimbursement", + "comment": "Some comment\n with multiple\n newlines", } +if len(sys.argv) < 3: + print("Error: Missing arguments") + print(f"Usage: python3 {sys.argv[0]} signature_file, attachment_files") + print(f"Output: output.pdf") + sys.exit(1) -def main(data, out): - data = modify_data(data) +# Parse the command line arguments +signature_file = sys.argv[1] +attachment_files = sys.argv[2:] - pdf = create_pdf(data) +allowed_extensions = {".pdf", ".jpg", ".jpeg", ".png", ".gif", ".heic"} - with open(out, "wb") as f: - f.write(pdf.encode("latin-1")) - print("Done!") +def is_valid_file_extension(file_path, allowed_extensions): + _, file_extension = os.path.splitext(file_path) + return file_extension.lower() in allowed_extensions -def encode_image(img): - with open(img, "rb") as f: - b64 = base64.b64encode(f.read()).decode("ascii") - return f'data:image/{img.split(".")[-1]};base64,{b64}' +# Return exception if signature or attachment files are not valid +if not is_valid_file_extension(signature_file, allowed_extensions): + raise Exception(f"Invalid signature file extension: {signature_file}") +for file_path in attachment_files: + if not is_valid_file_extension(file_path, allowed_extensions): + raise Exception(f"Invalid attachment file extension: {file_path}") +# Convert signature file to base64:image/png +with open(signature_file, "rb") as f: + signature = f.read() + signature = base64.b64encode(signature).decode("utf-8") + signature = f"data:image/png;base64,{signature}" -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("signature", help="Path to signature") - parser.add_argument("out", help="Path to the generated pdf") - parser.add_argument( - "images", nargs=argparse.REMAINDER, default=[], help="Paths to images" - ) - args = parser.parse_args() +# Check file type and convert attachment files to base64 with MIME type +attachments = [] +for file_path in attachment_files: + with open(file_path, "rb") as f: + # Read the file as bytes + file_data = f.read() - data = { - **default_data, - "signature": encode_image(args.signature), - "images": [encode_image(img) for img in args.images], - } + # Detect the filetype using python-magic + file_type = magic.from_buffer(file_data, mime=True) - main(data, args.out) + # Convert the file data to base64 + file_data = base64.b64encode(file_data).decode("utf-8") + + # Add the filetype prefix to the base64 string + file_data = f"data:{file_type};base64,{file_data}" + + # Check if the filetype is one of the allowed ones + allowed_types = ["application/pdf", "image/jpeg", "image/png", "image/heic"] + if file_type in allowed_types: + # Append the file data to the attachments list + attachments.append(file_data) + +print(f"Signature: {signature[:50]}") +for attachment in attachments: + print(f"Attachment: {attachment[:50]}") + +# Call the create_pdf function to generate the PDF +create_pdf(test_data, signature, attachments) diff --git a/kaaf/handler.py b/kaaf/handler.py index 4418dcb..dfaf7cb 100644 --- a/kaaf/handler.py +++ b/kaaf/handler.py @@ -1,20 +1,12 @@ import base64 import logging -import io +import os import tempfile import mail -import functools -import operator - -from fpdf import FPDF -from PIL import Image +import fitz from sentry_sdk import configure_scope - -# Handle PDF files -from pdf2image import convert_from_path - -# Handle HEIC photoes -import pyheif +import io +from PIL import Image class UnsupportedFileException(Exception): @@ -32,6 +24,8 @@ class UnsupportedFileException(Exception): "comment": "Kommentar:", } +temporary_files = [] + def data_is_valid(data): fields = [ @@ -49,131 +43,151 @@ def data_is_valid(data): return [f for f in fields if f not in data or len(data[f]) == 0] -class PDF(FPDF): - def header(self): - self.image("images/ntnui.png", 10, 10, 33) - self.set_font("Arial", "B", 15) - self.ln(20) - - def footer(self): - self.set_y(-15) - self.set_font("Arial", "I", 8) - self.cell(0, 10, f"Side {str(self.page_no())}/{{nb}}", 0, 0, "C") - - -def image_to_byte_array(image: Image, fmt=None): - imgByteArr = io.BytesIO() - image.save(imgByteArr, format=fmt if fmt is not None else image.format) - imgByteArr = imgByteArr.getvalue() - return imgByteArr - - -def create_image_file(image): - """ - Take an image in BASE64 format and return a NamedTemporaryFile containing the image. - Will handle PNG, JPEG and GIF without any changes, as FPDF will handle those files - without problem. For PDFs we use pdf2image to convert each page to an image. For HEIC - pictures we use pyheif to convert it to a jpeg. - """ - - if not "image/" in image and not "application/pdf" in image: - raise UnsupportedFileException(image[:30]) - parts = image.split(";base64,") - decoded = base64.b64decode(parts[1]) - suffix = "pdf" if "application/pdf" in image else parts[0].split("image/")[1] - suffix = suffix.lower() - f = tempfile.NamedTemporaryFile(suffix=f".{suffix}") - f.write(decoded) - f.flush() - - """ - FPDF does not support pdf files as input, therefore convert file:pdf to array[image:jpg] - """ - if suffix == "pdf": - files = [] - pil_images = convert_from_path(f.name, fmt="jpeg") - for img in pil_images: - f = tempfile.NamedTemporaryFile(suffix=f".{suffix}") - f.write(image_to_byte_array(img)) - files.append({"file": f, "type": "jpeg"}) - f.flush() - return files - - """ - FPDF does not support heic files as input, therefore we covert a image:heic image:jpg - """ - if suffix == "heic": - fmt = "JPEG" - heif_file = pyheif.read(f.name) - img = Image.frombytes( - heif_file.mode, - heif_file.size, - heif_file.data, - "raw", - heif_file.mode, - heif_file.stride, - ) - f = tempfile.NamedTemporaryFile(suffix=f".{fmt}") - f.write(image_to_byte_array(img, fmt)) - f.flush() - return [{"file": f, "type": fmt}] +def data_to_str(data, field_title_map): + left_column = [] + right_column = [] + + for key, title in field_title_map.items(): + if key in data: + left_column.append(f"{title}") + right_column.append(f"{data[key]}") + + left_text = "\n\n".join(left_column) + right_text = "\n\n".join(right_column) + + return left_text, right_text + + +# Decode the base64 string and save it to a temporary file +def base64_to_file(base64_string): + # Decode the base64 string + decoded = base64.b64decode(base64_string) + # Create a temporary file + temp_file = tempfile.NamedTemporaryFile(delete=False) + temporary_files.append(temp_file.name) + # Write the decoded data to the temporary file + temp_file.write(decoded) + # Close the file + temp_file.close() + # Return the path to the temporary file + return temp_file.name + +def add_page_number(page, page_number, total_pages): + footer_text = f"Side {page_number} av {total_pages}" + fontsize = 9 + fontname = "Helvetica" + + # Measure the width of the rendered footer_text + font = fitz.Font(fontname) + text_width = font.text_length(footer_text, fontsize) + + footer_position = fitz.Point( + (page.rect.width - text_width) / 2, + page.rect.height - 30 + ) + page.insert_text( + footer_position, footer_text, fontname=fontname, fontsize=fontsize + ) + - return [{"file": f, "type": suffix.upper()}] +def create_pdf(data, signature=None, images=None): + doc = fitz.open() + page = doc.new_page() + page.insert_text( + fitz.Point(50, 75), "Refusjonsskjema", fontname="Helvetica-Bold", fontsize=24 + ) -def modify_data(data): - signature = data.pop("signature") - images = data.pop("images") + logo = fitz.Pixmap("images/ntnui.png") + page.insert_image(fitz.Rect(425, 40, 525, 90), pixmap=logo) - data["signature"] = create_image_file(signature)[0] - data["images"] = functools.reduce( - operator.iconcat, [create_image_file(img) for img in images], [] + # Add the input values in a two-column layout + left_text, right_text = data_to_str(data, field_title_map) + page.insert_text( + fitz.Point(50, 150), left_text, fontname="Helvetica-Bold", fontsize=11 + ) + page.insert_text( + fitz.Point(250, 150), right_text, fontname="Helvetica", fontsize=11 ) - return data - - -def create_pdf(data): - pdf = PDF() - pdf.alias_nb_pages() - pdf.add_page() - pdf.set_font("Arial", "B", 16) - - signature = data.pop("signature") - images = data.pop("images") - - pdf.cell(0, 14, "Refusjonsskjema", ln=1) - - pdf.set_font("Arial", "", 12) - data["amount"] = data["amount"].replace(".", ",") # Format amount to Norwegian standard - for key in field_title_map.keys(): - pdf.set_font("", "B") - pdf.cell(90, 8, txt=field_title_map[key]) - pdf.set_font("", "") - pdf.multi_cell(0, 8, txt=data[key]) - - pdf.set_font("", "B") - pdf.cell(0, 20, txt="Signatur:", ln=1) - pdf.image(signature["file"].name, h=30, type=signature["type"]) - signature["file"].close() - pdf.cell(0, 5, txt="", ln=1) - pdf.cell(0, 20, txt="Vedlegg:", ln=1) - max_img_width = 190 - max_img_height = 220 - for image in images: - img = Image.open(image["file"].name) - w, h = img.size - img.close() - - size = ( - {"w": max_img_width} - if w / h >= max_img_width / max_img_height - else {"h": max_img_height} + # Add the signature image + if signature is None: + raise RuntimeError("No signature provided") + if signature.startswith("data:image"): + signature = base64_to_file(signature.split(",")[1]) + page.insert_text( + fitz.Point(50, page.bound().height * 0.67), + "Signatur:", + fontname="Helvetica-Bold", + fontsize=12, + ) + signature_pixmap = fitz.Pixmap(signature) + signature_rect = fitz.Rect( + 50, page.bound().height * 0.67, 550, page.bound().height * 0.97 + ) + page.insert_image(signature_rect, pixmap=signature_pixmap) + + # Add the remaining pages with the receipt attachments + if images is None: + raise RuntimeError("No images provided") + if not isinstance(images, list): + images = [images] + for attachment in images: + # Get file type from base64 string + if not "image/" in attachment and not "application/pdf" in attachment: + raise UnsupportedFileException( + f"Unsupported file type in base64 string: {attachment[:30]}" + ) + parts = attachment.split(";base64,") + file_type = ( + "pdf" if "application/pdf" in attachment else parts[0].split("image/")[1] ) - - pdf.image(image["file"].name, **size, type=image["type"]) - image["file"].close() - return pdf.output(dest="S") + attachment = base64_to_file(parts[1]) + if file_type == "pdf": + pdf_doc = fitz.open(attachment) + for i in range(pdf_doc.page_count): + page = doc.new_page() + page.show_pdf_page(fitz.Rect(0, 0, 612, 792), pdf_doc, i) + pdf_doc.close() + elif file_type in ["jpg", "jpeg", "png", "gif"]: + page = doc.new_page() + pixmap = fitz.Pixmap(attachment) + page.insert_image(page.rect, pixmap=pixmap) + ## TODO: HEIC is received as application/octet-stream, not as image/heic + # elif file_type == 'heic': + # heif_image = pyheif.read(attachment) + # png_image = Image.frombytes( + # heif_image.mode, + # heif_image.size, + # heif_image.data, + # "raw", + # heif_image.mode, + # heif_image.stride, + # ) + # png_bytes = io.BytesIO() + # png_image.save(png_bytes, format="PNG") + # width, height = png_image.size + # samples = png_image.tobytes() + # pixmap = fitz.Pixmap(fitz.csRGB, width, height, samples) + # page.insert_image(page.rect, pixmap=pixmap) + else: + raise UnsupportedFileException( + f"Unsupported file type: {file_type}. Use pdf, jpg, jpeg or png" + ) + + # Add page numbers to all pages + for i, page in enumerate(doc): + add_page_number(page, i + 1, doc.page_count) + + # Save the PDF document + doc.save("output.pdf") + with open("output.pdf", "rb") as pdf_file: + pdf_bytes = pdf_file.read() + doc.close() + for f in temporary_files: + os.remove(f) + temporary_files.remove(f) + return pdf_bytes # Return the PDF document as bytes def handle(data): @@ -189,16 +203,7 @@ def handle(data): return f'Requires fields {", ".join(req_fields)}', 400 try: - data = modify_data(data) - except UnsupportedFileException as e: - logging.error(f"Unsupported file type: {e}") - return ( - "En av filene som ble lastet opp er ikke i støttet format. Bruk PNG, JPEG, GIF, HEIC eller PDF", - 400, - ) - - try: - file = create_pdf(data) + file = create_pdf(data, data["signature"], data["images"]) mail.send_mail([data["mailto"], data["mailfrom"]], data, file) except RuntimeError as e: logging.warning(f"Failed to generate pdf with exception: {e}") diff --git a/kaaf/mail.py b/kaaf/mail.py index da1400e..5eda767 100644 --- a/kaaf/mail.py +++ b/kaaf/mail.py @@ -1,3 +1,4 @@ +from io import BytesIO import logging import os import json @@ -17,16 +18,16 @@ class MailConfigurationException(Exception): def service_account_login(mail_from, service_account_str): - SCOPES = ['https://www.googleapis.com/auth/gmail.send'] - credentials = service_account.Credentials.from_service_account_info(json.loads(base64.b64decode(service_account_str)), scopes=SCOPES) + SCOPES = ["https://www.googleapis.com/auth/gmail.send"] + credentials = service_account.Credentials.from_service_account_info( + json.loads(base64.b64decode(service_account_str)), scopes=SCOPES + ) delegated_credentials = credentials.with_subject(mail_from) - return build('gmail', 'v1', credentials=delegated_credentials) + return build("gmail", "v1", credentials=delegated_credentials) def create_mail(msg, body): - msg[ - "Subject" - ] = f'Refusjonsskjema - {body.get("name", "")}' + msg["Subject"] = f'Refusjonsskjema - {body.get("name", "")}' text = "" text += f'Navn: {body.get("name", "")}\n' @@ -37,7 +38,7 @@ def create_mail(msg, body): text += f'Dato: {body.get("date", "")}\n' text += f'Anledning/arrangement: {body.get("occasion", "")}\n' text += f'Kommentar: {body.get("comment", "")}\n' - text += f'\n' + text += f"\n" text += f"Refusjonsskjema er generert og vedlagt. Ved spørsmål ta kontakt med kasserer@ntnui.no!" msg.attach(MIMEText(text)) @@ -56,7 +57,9 @@ def send_mail(mail_to, body, file): create_mail(msg, body) - filename = body.get("date", "") + " Refusjonsskjema " + body.get("name", "") + ".pdf" + filename = ( + body.get("date", "") + " Refusjonsskjema " + body.get("name", "") + ".pdf" + ) part = MIMEApplication(file, Name=filename) part["Content-Disposition"] = f'attachment; filename="{filename}"' msg.attach(part) @@ -65,6 +68,6 @@ def send_mail(mail_to, body, file): service = service_account_login(mail_from, service_account_str) raw = base64.urlsafe_b64encode(msg.as_bytes()) - body = { 'raw': raw.decode() } + body = {"raw": raw.decode()} messages = service.users().messages() messages.send(userId="me", body=body).execute() diff --git a/kaaf/req.txt b/kaaf/req.txt index d054637..86a534c 100644 --- a/kaaf/req.txt +++ b/kaaf/req.txt @@ -1,10 +1,14 @@ -flask -gevent -fpdf -black -Pillow +flask==2.3.2 +gevent==22.10.2 +PyMuPDF==1.22.2 +pymupdf-fonts==1.0.5 +fonttools==4.39.3 +black==23.3.0 +Pillow==9.5.0 sentry-sdk[flask]==0.16.2 -pdf2image -pyheif -google -google-api-python-client \ No newline at end of file +pdf2image==1.16.3 +# pyheif # not available on Windows, and HEIC attachments is broken +google==3.0.0 +google-api-python-client==2.86.0 +python-magic==0.4.27 +python-dotenv==1.0.0 diff --git a/kaaf/server.py b/kaaf/server.py index 95873bf..54d2ae2 100644 --- a/kaaf/server.py +++ b/kaaf/server.py @@ -6,6 +6,8 @@ from sentry_sdk.integrations.flask import FlaskIntegration from handler import handle +from dotenv import load_dotenv + static_file_directory = os.environ.get("STATIC_DIRECTORY", "../webapp/out/") @@ -15,6 +17,12 @@ environment=os.environ.get("ENVIRONMENT"), integrations=[FlaskIntegration()], ) +else: + if os.path.exists(".env") and os.path.getsize(".env") != 0: + print("✔ .env found") + load_dotenv(verbose=True) + else: + print("⚠ .env file not found, or is empty") app = Flask(__name__, static_folder=static_file_directory, static_url_path="") @@ -26,7 +34,7 @@ def fix_transfer_encoding(): """ transfer_encoding = request.headers.get("Transfer-Encoding", None) - if transfer_encoding == u"chunked": + if transfer_encoding == "chunked": request.environ["wsgi.input_terminated"] = True @@ -43,4 +51,5 @@ def main_route(): if __name__ == "__main__": http_server = WSGIServer(("", 5000), app) + print("✔ Server started at http://localhost:5000/") http_server.serve_forever() diff --git a/webapp/components/Form.tsx b/webapp/components/Form.tsx index 3744bc5..81b5565 100644 --- a/webapp/components/Form.tsx +++ b/webapp/components/Form.tsx @@ -68,6 +68,7 @@ const Form = (): JSX.Element => { { { { -/// /// // NOTE: This file should not be edited