From e9eca452e8e1fe7ca0d0884a608a7f31201a43ea Mon Sep 17 00:00:00 2001 From: Robert Metcalf <metcalfr@cardiff.ac.uk> Date: Fri, 3 Dec 2021 10:50:44 +0000 Subject: [PATCH] add CSV upload interface, support updating workspaces in place --- database.py | 115 +++++++++++++++++++----- import_database.py | 2 +- main.py | 38 ++++++++ templates/import-workspaces-result.html | 7 ++ templates/import-workspaces.html | 8 ++ 5 files changed, 148 insertions(+), 22 deletions(-) create mode 100644 templates/import-workspaces-result.html create mode 100644 templates/import-workspaces.html diff --git a/database.py b/database.py index 046e671..eb74b08 100644 --- a/database.py +++ b/database.py @@ -90,24 +90,48 @@ class Workspace: def validate(self): errors = [] - if len(self.name.strip()) <= 0: + if type(self.name) != str: + errors.append("Name must be a string") + elif len(self.name.strip()) <= 0: errors.append("Name must not be empty") - if len(self.address.strip()) <= 0: + if type(self.address) != str: + errors.append("Address must be a string") + elif len(self.address.strip()) <= 0: errors.append("Address must not be empty") - if len(self.main_photo.strip()) <= 0: + if type(self.main_photo) != str: + errors.append("Main photo must be a string") + elif len(self.main_photo.strip()) <= 0: errors.append("Main photo must not be empty") - if len(self.description.strip()) <= 0: + if type(self.description) != str: + errors.append("Description must be a string") + elif len(self.description.strip()) <= 0: errors.append("Description must not be empty") - if len(self.website.strip()) <= 0: + if type(self.website) != str: + errors.append("Website must be a string") + elif len(self.website.strip()) <= 0: errors.append("Website must not be empty") - if len(self.email.strip()) <= 0: + if type(self.email) != str: + errors.append("Email must be a string") + elif len(self.email.strip()) <= 0: errors.append("Email must not be empty") - if len(self.phone_number.strip()) <= 0: + if type(self.phone_number) != str: + errors.append("Phone number must be a string") + elif len(self.phone_number.strip()) <= 0: errors.append("Phone number must not be empty") - if len(self.opening_hours.strip()) <= 0: + if type(self.opening_hours) != str: + errors.append("Opening hours must be a string") + elif len(self.opening_hours.strip()) <= 0: errors.append("Opening hours must not be empty") + if type(self.additional_photos) != list: + errors.append("Additional photos must be a list") + + if type(self.checkin_instructions) != str: + errors.append("Checkin instructions must be a string") + for x in self.additional_photos: + if type(x) != str: + errors.append("Each edditional photos must be a string") if len(x.strip()) <= 0: errors.append("Each edditional photos must not be empty") @@ -115,27 +139,76 @@ class Workspace: def from_query(conn, tuple): (id, name, address, main_photo, description, website, email, phone_number, opening_hours, checkin_instructions) = tuple - additional_photos = [] + additional_photos = get_additional_photos_inner(conn, id) + + workspace = Workspace(name, address, main_photo, additional_photos, description, website, email, phone_number, opening_hours, checkin_instructions) + workspace.id = str(id) + return workspace +def get_additional_photos_inner(conn: Connection, id): + additional_photos = [] + + if id != None: conn.execute("SELECT url FROM AdditionalPhotos WHERE workspace_id = ?", (str(id))) for x in conn.cursor.fetchall(): additional_photos.append(x[0]) - workspace = Workspace(name, address, main_photo, additional_photos, description, website, email, phone_number, opening_hours, checkin_instructions) - workspace.id = id - return workspace + return additional_photos + +def add_additional_photos_inner(conn: Connection, workspace: Workspace): + old_photos = get_additional_photos_inner(conn, workspace.id) + for url in old_photos: + if url not in workspace.additional_photos: + conn.execute("DELETE FROM AdditionalPhotos WHERE url = ? AND workspace_id = ?", (url, workspace.id)) + + for url in workspace.additional_photos: + if url not in old_photos: + conn.execute("INSERT INTO AdditionalPhotos (url, workspace_id) VALUES (?, ?)", (url, workspace.id)) + +def add_workspace_inner(conn: Connection, workspace: Workspace): + conn.execute( + "INSERT INTO Workspaces (name, address, main_photo, description, website, email, phone_number, opening_hours, checkin_instructions) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + (workspace.name, workspace.address, workspace.main_photo, workspace.description, workspace.website, workspace.email, workspace.phone_number, workspace.opening_hours, workspace.checkin_instructions) + ) + id = conn.cursor.lastrowid + workspace.id = str(id) + add_additional_photos_inner(conn, workspace) + conn.commit() + return id def add_workspace(workspace: Workspace): with Connection() as conn: - conn.execute( - "INSERT INTO Workspaces (name, address, main_photo, description, website, email, phone_number, opening_hours, checkin_instructions) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", - (workspace.name, workspace.address, workspace.main_photo, workspace.description, workspace.website, workspace.email, workspace.phone_number, workspace.opening_hours, workspace.checkin_instructions) - ) - id = conn.cursor.lastrowid - for url in workspace.additional_photos: - conn.execute("INSERT INTO AdditionalPhotos (url, workspace_id) VALUES (?, ?)", (url, id)) - conn.commit() - return id + return add_workspace_inner(conn, workspace) + +def update_workspace_inner(conn: Connection, workspace: Workspace): + conn.execute( + "UPDATE Workspaces SET address = ?, main_photo = ?, description = ?, website = ?, email = ?, phone_number = ?, opening_hours = ?, checkin_instructions = ? WHERE name = ?", + (workspace.address, workspace.main_photo, workspace.description, workspace.website, workspace.email, workspace.phone_number, workspace.opening_hours, workspace.checkin_instructions, workspace.name) + ) + + if conn.cursor.rowcount == 0: + return None + + if workspace.id == None: + conn.execute("SELECT id FROM Workspaces WHERE name = ? LIMIT 1", (workspace.name,)) + id = conn.fetch_all()[0][0] + workspace.id = str(id) + + add_additional_photos_inner(conn, workspace) + conn.commit() + return id + +def update_workspace(workspace: Workspace): + with Connection() as conn: + return update_workspace_inner(conn, workspace) + +def add_or_update(workspace: Workspace): + with Connection() as conn: + id = update_workspace_inner(conn, workspace) + if id == None: + return (False, add_workspace_inner(conn, workspace)) + else: + return (True, id) def get_workspaces(): with Connection() as conn: diff --git a/import_database.py b/import_database.py index e5a0e23..8fa59fc 100644 --- a/import_database.py +++ b/import_database.py @@ -20,7 +20,7 @@ def import_workspace(data): if latlong != None: database.set_address_latlong(workspace.address, (latlong[0], latlong[1])) - database.add_workspace(workspace) + database.add_or_update(workspace) file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "workspaces.json") diff --git a/main.py b/main.py index d566b83..19c7226 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,7 @@ from flask import Flask, request, render_template, redirect import database import json +import csv import re app = Flask(__name__) @@ -82,5 +83,42 @@ def add_workspace(): return render_template("add-workspace-result.html", messages = messages) +@app.route("/admin/import-workspaces", methods = ["GET", "POST"]) +def import_workspaces(): + if request.method == "GET": + return render_template("import-workspaces.html") + + if request.method == "POST": + file = request.files["csv"] + reader = csv.DictReader([x.decode("utf-8-sig") for x in file]) + messages = [] + for entry in reader: + additional_photos = entry.get("additional_photo") + additional_photos = [x.strip() for x in additional_photos.split(",")] if type(additional_photos) == str and len(additional_photos) > 0 else [] + workspace = database.Workspace( + entry.get("name"), + entry.get("address"), + entry.get("main_photo"), + additional_photos, + entry.get("description"), + entry.get("website"), + entry.get("email"), + entry.get("phone_number"), + entry.get("opening_hours"), + entry.get("checkin_instructions") + ) + + errors = workspace.validate() + if len(errors) > 0: + messages.append(f"Errors were found with {workspace.name}:") + messages.extend(errors) + else: + update = database.add_or_update(workspace)[0] + messages.append(f"Workspace updated successfully ({workspace.name})" if update else f"Workspace added successfully ({workspace.name})") + + if len(messages) == 0: + messages = ["No workspaces were found"] + return render_template("import-workspaces-result.html", messages = messages) + if __name__ == "__main__": app.run(debug = True) diff --git a/templates/import-workspaces-result.html b/templates/import-workspaces-result.html new file mode 100644 index 0000000..e7550f4 --- /dev/null +++ b/templates/import-workspaces-result.html @@ -0,0 +1,7 @@ +{% extends "main.html" %} +{% block title %}Import Workspaces{% endblock %} +{% block mainBlock %} +{% for message in messages %} +<div>{{ message }}</div> +{% endfor %} +{% endblock %} \ No newline at end of file diff --git a/templates/import-workspaces.html b/templates/import-workspaces.html new file mode 100644 index 0000000..bc6a45e --- /dev/null +++ b/templates/import-workspaces.html @@ -0,0 +1,8 @@ +{% extends "main.html" %} +{% block title %}Import Workspaces{% endblock %} +{% block mainBlock %} +<form action="/admin/import-workspaces" method="POST" enctype="multipart/form-data"> + <div><input type="file" name="csv"></div> + <div><input type="submit"></div> +</form> +{% endblock %} \ No newline at end of file -- GitLab