snto

Swedish National Test Organizer
git clone https://noxz.tech/git/snto.git
snto

commit: 3417e539dfa27b6462f986960d2a853ef5cd3ae7
parent: ea99c4abc110d7ca06f7e40501c3ac3ba7bab942
author: Chris Noxz <chris@noxz.tech>
date:   Mon, 4 Dec 2023 13:57:24 +0100
create web server
Aserve.py416++++++++++++++++++++
1 file changed, 416 insertions(+)
diff --git a/serve.py b/serve.py
@@ -0,0 +1,416 @@
+#!/usr/bin/env python3
+
+import sqlite3
+import base64
+from http.server import BaseHTTPRequestHandler, HTTPServer
+from sqlite3 import Error
+from urllib.parse import urlparse, parse_qs
+
+hostName                = "localhost"
+serverPort              = 8080
+db_file                 = "npo.db"
+html_title              = "Nationella Prov"
+html_body               = """
+<html>
+	<head>
+		<title>{title}</title>
+		<style>{style}</style>
+	</head>
+	<body>
+		{content}
+	</body>
+</html>
+"""
+html_default            = """
+<div id="head">
+	<div>
+		<form action="/" method="get">
+			<label for="course">Kurs:</label>
+			<select name="course" id="course" onchange="this.form.submit();">
+				<option value="">alla</option>
+				{courses}
+			</select>
+			<label for="semester">Termin:</label>
+			<select name="semester" id="semester" onchange="this.form.submit();">
+				<option value="">alla</option>
+				{semesters}
+			</select>
+			{tags}
+		</form>
+	</div>
+</div>
+<div id="main">
+	<div>
+		{tasks}
+	</div>
+</div>
+"""
+html_default_task       = """
+<div class="task">
+	<div class="task-head">
+		<h1>{course}: {semester}</h1>
+		<h2>{tags}</h2>
+	</div>
+	<a href="{link}">
+		<img src="data:image/png;base64,{image}"/>
+	</a>
+</div>
+"""
+html_task               = """
+<div id="head" class="task">
+	<div class="task-head">
+		<h1>{course}: {semester}</h1>
+		<h2>{tags}</h2>
+	</div>
+</div>
+<div id="main">
+	{images}
+</div>
+"""
+css                     = """
+body {
+	background-color    : #d8dee9;
+	margin              : 0;
+}
+#head {
+	background-color    : #2e3440;
+	line-height         : 2em;
+}
+#head>div * {
+	vertical-align      : middle;
+}
+#head>div {
+	background-color    : #2e3440;
+	color               : #ffffff;
+	min-width           : 640px;
+	width               : 80%;
+	margin              : 0 auto;
+	padding             : 10px 0px 5px;
+}
+#head label {
+	margin-right        : 10px;
+}
+#head select {
+	margin-right        : 20px;
+}
+#main {
+	min-width           : 640px;
+	width               : 80%;
+	margin              : 0 auto;
+	padding             : 20px;
+}
+#main .task, #main .image {
+	background-color    : #ffffff;
+	padding             : 10px;
+	margin-bottom       : 20px;
+}
+#main .task .task-head {
+	background-color    : #2e3440;
+	color               : #ffffff;
+	padding             : 1px 20px;
+}
+#main .task .task-head h1 {
+	font-size           : 1.5em;
+}
+#main .task .task-head h2 {
+	font-size           : 1em;
+}
+.tag {
+	display             : inline;
+	white-space         : nowrap;
+}
+img {
+	max-width           : 100%
+}
+"""
+
+class Task:
+	def __init__(self, row):
+		self.id         = row[0]
+		self.course     = row[1]
+		self.semester   = row[2]
+		self.number     = row[3]
+		self.tags       = []
+		self.images     = []
+
+	def append_tag(self, tag):
+		self.tags.append(tag)
+
+	def append_image(self, image):
+		self.images.append(image)
+
+	def load(taskid):
+		return DataManager.select_task(taskid)
+
+	def all(course, semester):
+		return list(DataManager.select_all_tasks(course, semester))
+
+
+class DataManager:
+	def select_all_tags(course, semester):
+		connection = None
+		try:
+			connection = sqlite3.connect(db_file)
+			cursor = connection.cursor()
+			cursor.execute("""
+				SELECT DISTINCT tagname
+				FROM tag
+				INNER JOIN tasktag, task
+					ON tasktag.tagid=tag.id
+					AND tasktag.taskid=task.id
+				{where}
+				ORDER BY tagname
+				""".format(where = ""
+					if (not course and not semester)
+					else "WHERE course=?" if (course and not semester)
+					else "WHERE semester=?" if (semester and not course)
+					else "WHERE course=? AND semester=?"
+				), () if (not course and not semester)
+					else (course, ) if (course and not semester)
+					else (semester, ) if (semester and not course)
+					else (course, semester, ))
+			for row in cursor.fetchall():
+				yield row[0]
+			cursor.close()
+		except sqlite3.Error as e:
+			print("Failed to read data:", e)
+		finally:
+			if connection:
+				connection.close()
+
+	def select_all_courses(semester):
+		connection = None
+		try:
+			connection = sqlite3.connect(db_file)
+			cursor = connection.cursor()
+			cursor.execute("""
+				SELECT DISTINCT course
+				FROM task
+				{where}
+				ORDER BY course
+				""".format(where = "WHERE semester=?" if semester else ""),
+				(semester, ) if semester else ())
+			for row in cursor.fetchall():
+				yield row[0]
+			cursor.close()
+		except sqlite3.Error as e:
+			print("Failed to read data:", e)
+		finally:
+			if connection:
+				connection.close()
+
+	def select_all_semesters(course):
+		connection = None
+		try:
+			connection = sqlite3.connect(db_file)
+			cursor = connection.cursor()
+			cursor.execute("""
+				SELECT DISTINCT semester
+				FROM task
+				{where}
+				ORDER BY semester
+				""".format(where = "WHERE course=?" if course else ""),
+				(course, ) if course else ())
+			for row in cursor.fetchall():
+				yield row[0]
+			cursor.close()
+		except sqlite3.Error as e:
+			print("Failed to read data:", e)
+		finally:
+			if connection:
+				connection.close()
+
+	def select_task_data(row, connection):
+		task = Task(row)
+
+		try:
+			cursor = connection.cursor()
+			cursor.execute("""
+				SELECT tagname
+				FROM tag
+				INNER JOIN tasktag ON tasktag.tagid=tag.id
+				WHERE tasktag.taskid=?
+				ORDER BY tagname
+				""", (task.id, ))
+			for row in cursor.fetchall():
+				task.append_tag(row[0])
+			cursor.close()
+
+			cursor = connection.cursor()
+			cursor.execute("""
+				SELECT *
+				FROM image
+				WHERE taskid=?
+				ORDER BY idx
+				""", (task.id, ))
+			for row in cursor.fetchall():
+				task.append_image(row[3])
+			cursor.close()
+		except sqlite3.Error as e:
+			print("Failed to read data:", e)
+
+		return task
+
+	def select_task(taskid):
+		connection = None
+		task = None
+		try:
+			connection = sqlite3.connect(db_file)
+			cursor = connection.cursor()
+			cursor.execute("""
+				SELECT *
+				FROM task
+				WHERE id=?
+				""", (taskid, ))
+			row = cursor.fetchone()
+			task = DataManager.select_task_data(row, connection) if row else None
+			cursor.close()
+		except sqlite3.Error as e:
+			print("Failed to read data:", e)
+		finally:
+			if connection:
+				connection.close()
+		return task
+
+	def select_all_tasks(course, semester):
+		connection = None
+		try:
+			connection = sqlite3.connect(db_file)
+			cursor = connection.cursor()
+			cursor.execute("""
+				SELECT *
+				FROM task
+				{where}
+				""".format(where = ""
+					if (not course and not semester)
+					else "WHERE course=?" if (course and not semester)
+					else "WHERE semester=?" if (semester and not course)
+					else "WHERE course=? AND semester=?"
+				), () if (not course and not semester)
+					else (course, ) if (course and not semester)
+					else (semester, ) if (semester and not course)
+					else (course, semester, ))
+			for row in cursor.fetchall():
+				yield DataManager.select_task_data(row, connection)
+			cursor.close()
+
+		except sqlite3.Error as e:
+			print("Failed to read data:", e)
+		finally:
+			if connection:
+				connection.close()
+
+
+class WebServer(BaseHTTPRequestHandler):
+	def do_HEADERS(self):
+		self.send_response(200)
+		self.send_header("Content-type", "text/html")
+		self.end_headers()
+
+	def do_GET(self):
+		self.do_HEADERS()
+		self.wfile.write(bytes(html_body.format(
+			title = html_title,
+			style = css,
+			content = self.task_page()
+			          if self.path.startswith("/task")
+			          else self.default_page()
+		), "utf-8"))
+
+	def default_page_courses(self, qs, courses):
+		html = ""
+		for course in courses:
+			html += """
+				<option {selected} value="{course}">{course}</option>
+			""".format(
+				selected = "selected=\"selected\"" if qs["course"] == course else "",
+				course = course
+			)
+		return html
+
+	def default_page_semesters(self, qs, semesters):
+		html = ""
+		for semester in semesters:
+			html += """
+				<option {selected} value="{semester}">{semester}</option>
+			""".format(
+				selected = "selected=\"selected\"" if qs["semester"] == semester else "",
+				semester = semester
+			)
+		return html
+
+	def default_page_tags(self, qs, tags):
+		html = ""
+		for tag in tags:
+			html += """
+				<div class="tag">
+					<input onchange="this.form.submit();" {checked} name="tag" type="checkbox" id="tag_{tag}" value="{tag}" />
+					<label for="tag_{tag}">{tag}</label>
+				</div>
+			""".format(
+				checked = "checked=\"checked\"" if tag in qs["tags"] else "",
+				tag = tag
+			)
+		return html
+
+	def default_page_tasks(self, tasks):
+		html = ""
+		for task in tasks:
+			html += html_default_task.format(
+				course = task.course,
+				semester = task.semester,
+				tags = ", ".join(task.tags),
+				link = "/task?id={taskid}".format(taskid = task.id),
+				image = base64.b64encode(task.images[0]).decode("ascii")
+			)
+		return html
+
+	def default_page(self):
+		qs_raw      = parse_qs(urlparse(self.path).query)
+		qs          = {
+		                "course"   : qs_raw["course"][0] if "course" in qs_raw.keys() else "",
+		                "semester" : qs_raw["semester"][0] if "semester" in qs_raw.keys() else "",
+		              }
+		courses     = list(DataManager.select_all_courses(qs["semester"]))
+		semesters   = list(DataManager.select_all_semesters(qs["course"]))
+		tags        = list(DataManager.select_all_tags(qs["course"], qs["semester"]))
+		qs["tags"]  = set(tags) & set(qs_raw["tag"] if "tag" in qs_raw.keys() else "")
+		tasks       = (task
+		               for task in Task.all(qs["course"], qs["semester"])
+		               if len(qs["tags"]) == 0
+		               or len(set(task.tags) & set(qs["tags"])) == len(qs["tags"]))
+
+		return html_default.format(
+			courses     = self.default_page_courses(qs, courses),
+			semesters   = self.default_page_semesters(qs, semesters),
+			tags        = self.default_page_tags(qs, tags),
+			tasks       = self.default_page_tasks(tasks)
+		)
+
+	def task_page(self):
+		qs_raw      = parse_qs(urlparse(self.path).query)
+		task        = Task.load(qs_raw["id"][0] if "id" in qs_raw.keys() else "")
+
+		return html_task.format(
+			course      = task.course,
+			semester    = task.semester,
+			tags        = ", ".join(task.tags),
+			images      = "".join("""
+				<div class="image">
+					<img src="data:image/png;base64,%s"/>
+				</div>
+			""" % base64.b64encode(image).decode("ascii") for image in task.images)
+		) if task else "Error: task does not exist"
+
+
+if __name__ == "__main__":
+	webServer = HTTPServer((hostName, serverPort), WebServer)
+	print("Server started at http://%s:%s" % (hostName, serverPort))
+
+	try:
+		webServer.serve_forever()
+	except KeyboardInterrupt:
+		pass
+
+	webServer.server_close()
+	print("Server stopped.")