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