snto

Swedish National Test Organizer
git clone https://noxz.tech/git/snto.git
Log | Files | README | LICENSE

serve.py
1#!/usr/bin/env python3
2
3import sqlite3
4import base64
5from http.server import BaseHTTPRequestHandler, HTTPServer
6from sqlite3 import Error
7from urllib.parse import urlparse, parse_qs
8
9hostName                = "localhost"
10serverPort              = 8080
11db_file                 = "npo.db"
12html_title              = "Nationella Prov"
13html_body               = """
14<html>
15	<head>
16		<title>{title}</title>
17		<style>{style}</style>
18	</head>
19	<body>
20		{content}
21	</body>
22</html>
23"""
24html_default            = """
25<div id="head">
26	<div>
27		<form action="/" method="get">
28			<label for="course">Kurs:</label>
29			<select name="course" id="course" onchange="this.form.submit();">
30				<option value="">alla</option>
31				{courses}
32			</select>
33			<label for="semester">Termin:</label>
34			<select name="semester" id="semester" onchange="this.form.submit();">
35				<option value="">alla</option>
36				{semesters}
37			</select>
38			{tags}
39		</form>
40	</div>
41</div>
42<div id="main">
43	<div>
44		{tasks}
45	</div>
46</div>
47"""
48html_default_task       = """
49<div class="task">
50	<a href="{link}">
51	<span class="task-head">
52		<h1>{course}: {semester}</h1>
53		<h2>{tags}</h2>
54	</span>
55	<span class="image">
56		<img src="{image}"/>
57	</span>
58	</a>
59</div>
60"""
61html_image              = """
62<div class="image">
63	<img src="{image}"/>
64</div>
65"""
66html_task               = """
67<div id="head" class="task">
68	<span class="task-head">
69		<h1>{course}: {semester}</h1>
70		<h2>{tags}</h2>
71	</span>
72</div>
73<div id="main">
74	{images}
75</div>
76"""
77css                     = """
78body {
79	background-color    : #d8dee9;
80	margin              : 0;
81}
82#head {
83	background-color    : #2e3440;
84	line-height         : 2em;
85}
86#head>div * {
87	vertical-align      : middle;
88}
89#head>div {
90	background-color    : #2e3440;
91	color               : #ffffff;
92	min-width           : 640px;
93	width               : 80%;
94	margin              : 0 auto;
95	padding             : 10px 0px 5px;
96}
97#head label {
98	margin-right        : 10px;
99}
100#head select {
101	margin-right        : 20px;
102}
103#main {
104	min-width           : 640px;
105	width               : 80%;
106	margin              : 0 auto;
107	padding             : 20px;
108}
109#main .image {
110	display             : block;
111	background-color    : #ffffff;
112	padding             : 10px;
113	margin-bottom       : 20px;
114}
115.task .task-head {
116	display             : block;
117	background-color    : #2e3440;
118	color               : #ffffff;
119	padding             : 1px 20px;
120}
121.task .task-head h1 {
122	font-size           : 1.5em;
123}
124.task .task-head h2 {
125	font-size           : 1em;
126}
127.task a {
128	text-decoration     : none;
129}
130.task a:hover .task-head {
131	background-color    : #3b4252;
132}
133.tag {
134	display             : inline;
135	white-space         : nowrap;
136}
137img {
138	max-width           : 100%;
139}
140"""
141
142class Task:
143	def __init__(self, row):
144		self.id         = row[0]
145		self.course     = row[1]
146		self.semester   = row[2]
147		self.number     = row[3]
148		self.tags       = []
149		self.images     = []
150
151	def append_tag(self, tag):
152		self.tags.append(tag)
153
154	def append_image(self, image):
155		self.images.append(image)
156
157	def load(taskid):
158		return DataManager.select_task(taskid)
159
160	def all(course, semester):
161		return list(DataManager.select_all_tasks(course, semester))
162
163
164class DataManager:
165	def select_all_tags(course, semester):
166		connection = None
167		try:
168			connection = sqlite3.connect(db_file)
169			cursor = connection.cursor()
170			cursor.execute("""
171				SELECT DISTINCT tagname
172				FROM tag
173				INNER JOIN tasktag, task
174					ON tasktag.tagid=tag.id
175					AND tasktag.taskid=task.id
176				{where}
177				ORDER BY tagname
178				""".format(where = ""
179					if (not course and not semester)
180					else "WHERE course=?" if (course and not semester)
181					else "WHERE semester=?" if (semester and not course)
182					else "WHERE course=? AND semester=?"
183				), () if (not course and not semester)
184					else (course, ) if (course and not semester)
185					else (semester, ) if (semester and not course)
186					else (course, semester, ))
187			for row in cursor.fetchall():
188				yield row[0]
189			cursor.close()
190		except sqlite3.Error as e:
191			print("Failed to read data:", e)
192		finally:
193			if connection:
194				connection.close()
195
196	def select_all_courses(semester):
197		connection = None
198		try:
199			connection = sqlite3.connect(db_file)
200			cursor = connection.cursor()
201			cursor.execute("""
202				SELECT DISTINCT course
203				FROM task
204				{where}
205				ORDER BY course
206				""".format(where = "WHERE semester=?" if semester else ""),
207				(semester, ) if semester else ())
208			for row in cursor.fetchall():
209				yield row[0]
210			cursor.close()
211		except sqlite3.Error as e:
212			print("Failed to read data:", e)
213		finally:
214			if connection:
215				connection.close()
216
217	def select_all_semesters(course):
218		connection = None
219		try:
220			connection = sqlite3.connect(db_file)
221			cursor = connection.cursor()
222			cursor.execute("""
223				SELECT DISTINCT semester
224				FROM task
225				{where}
226				ORDER BY semester
227				""".format(where = "WHERE course=?" if course else ""),
228				(course, ) if course else ())
229			for row in cursor.fetchall():
230				yield row[0]
231			cursor.close()
232		except sqlite3.Error as e:
233			print("Failed to read data:", e)
234		finally:
235			if connection:
236				connection.close()
237
238	def select_task_data(row, connection):
239		task = Task(row)
240
241		try:
242			cursor = connection.cursor()
243			cursor.execute("""
244				SELECT tagname
245				FROM tag
246				INNER JOIN tasktag ON tasktag.tagid=tag.id
247				WHERE tasktag.taskid=?
248				ORDER BY tagname
249				""", (task.id, ))
250			for row in cursor.fetchall():
251				task.append_tag(row[0])
252			cursor.close()
253
254			cursor = connection.cursor()
255			cursor.execute("""
256				SELECT *
257				FROM image
258				WHERE taskid=?
259				ORDER BY idx
260				""", (task.id, ))
261			for row in cursor.fetchall():
262				task.append_image(row[0])
263			cursor.close()
264		except sqlite3.Error as e:
265			print("Failed to read data:", e)
266
267		return task
268
269	def select_task(taskid):
270		connection = None
271		task = None
272		try:
273			connection = sqlite3.connect(db_file)
274			cursor = connection.cursor()
275			cursor.execute("""
276				SELECT *
277				FROM task
278				WHERE id=?
279				""", (taskid, ))
280			row = cursor.fetchone()
281			task = DataManager.select_task_data(row, connection) if row else None
282			cursor.close()
283		except sqlite3.Error as e:
284			print("Failed to read data:", e)
285		finally:
286			if connection:
287				connection.close()
288		return task
289
290	def select_all_tasks(course, semester):
291		connection = None
292		try:
293			connection = sqlite3.connect(db_file)
294			cursor = connection.cursor()
295			cursor.execute("""
296				SELECT *
297				FROM task
298				{where}
299				""".format(where = ""
300					if (not course and not semester)
301					else "WHERE course=?" if (course and not semester)
302					else "WHERE semester=?" if (semester and not course)
303					else "WHERE course=? AND semester=?"
304				), () if (not course and not semester)
305					else (course, ) if (course and not semester)
306					else (semester, ) if (semester and not course)
307					else (course, semester, ))
308			for row in cursor.fetchall():
309				yield DataManager.select_task_data(row, connection)
310			cursor.close()
311
312		except sqlite3.Error as e:
313			print("Failed to read data:", e)
314		finally:
315			if connection:
316				connection.close()
317
318	def select_image(imageid):
319		connection = None
320		image = None
321		try:
322			connection = sqlite3.connect(db_file)
323			cursor = connection.cursor()
324			cursor.execute("""
325				SELECT image
326				FROM image
327				WHERE id = ?
328				""", (imageid, ))
329			row = cursor.fetchone()
330			image = row[0] if row else None
331			cursor.close()
332
333		except sqlite3.Error as e:
334			print("Failed to read data:", e)
335		finally:
336			if connection:
337				connection.close()
338		return image
339
340
341class WebServer(BaseHTTPRequestHandler):
342	def do_HEADERS(self):
343		self.send_response(200)
344		if self.path.startswith("/image"):
345			self.send_header("Content-type", "image/png")
346		else:
347			self.send_header("Content-type", "text/html")
348		self.end_headers()
349
350	def do_GET(self):
351		self.do_HEADERS()
352		self.wfile.write(bytes(html_body.format(
353			title = html_title,
354			style = css,
355			content =      self.task_page()    if self.path.startswith("/task")
356			          else self.render_image() if self.path.startswith("/image")
357			          else self.default_page()
358		), "utf-8"))
359
360	def default_page_courses(self, qs, courses):
361		html = ""
362		for course in courses:
363			html += """
364				<option {selected} value="{course}">{course}</option>
365			""".format(
366				selected = "selected=\"selected\"" if qs["course"] == course else "",
367				course = course
368			)
369		return html
370
371	def default_page_semesters(self, qs, semesters):
372		html = ""
373		for semester in semesters:
374			html += """
375				<option {selected} value="{semester}">{semester}</option>
376			""".format(
377				selected = "selected=\"selected\"" if qs["semester"] == semester else "",
378				semester = semester
379			)
380		return html
381
382	def default_page_tags(self, qs, tags):
383		html = ""
384		for tag in tags:
385			html += """
386				<div class="tag">
387					<input onchange="this.form.submit();" {checked} name="tag" type="checkbox" id="tag_{tag}" value="{tag}" />
388					<label for="tag_{tag}">{tag}</label>
389				</div>
390			""".format(
391				checked = "checked=\"checked\"" if tag in qs["tags"] else "",
392				tag = tag
393			)
394		return html
395
396	def default_page_tasks(self, tasks):
397		html = ""
398		for task in tasks:
399			html += html_default_task.format(
400				course   = task.course,
401				semester = task.semester,
402				tags     = ", ".join(task.tags),
403				link     = "/task?id={taskid}".format(taskid = task.id),
404				image    = "/image?id={imageid}".format(imageid = task.images[0])
405			)
406		return html
407
408	def default_page(self):
409		qs_raw      = parse_qs(urlparse(self.path).query)
410		qs          = {
411		                "course"   : qs_raw["course"][0] if "course" in qs_raw.keys() else "",
412		                "semester" : qs_raw["semester"][0] if "semester" in qs_raw.keys() else "",
413		              }
414		courses     = list(DataManager.select_all_courses(qs["semester"]))
415		semesters   = list(DataManager.select_all_semesters(qs["course"]))
416		tags        = list(DataManager.select_all_tags(qs["course"], qs["semester"]))
417		qs["tags"]  = set(tags) & set(qs_raw["tag"] if "tag" in qs_raw.keys() else "")
418		tasks       = (task
419		               for task in Task.all(qs["course"], qs["semester"])
420		               if len(qs["tags"]) == 0
421		               or len(set(task.tags) & set(qs["tags"])) == len(qs["tags"]))
422
423		return html_default.format(
424			courses     = self.default_page_courses(qs, courses),
425			semesters   = self.default_page_semesters(qs, semesters),
426			tags        = self.default_page_tags(qs, tags),
427			tasks       = self.default_page_tasks(tasks)
428		)
429
430	def task_page(self):
431		qs_raw      = parse_qs(urlparse(self.path).query)
432		task        = Task.load(qs_raw["id"][0] if "id" in qs_raw.keys() else "")
433
434		return html_task.format(
435			course      = task.course,
436			semester    = task.semester,
437			tags        = ", ".join(task.tags),
438			images      = "".join(html_image.format(
439			                image = "/image?id=%s" % image
440			              ) for image in task.images)
441		) if task else "Error: task does not exist"
442
443	def render_image(self):
444		qs_raw      = parse_qs(urlparse(self.path).query)
445		image       = DataManager.select_image(qs_raw["id"][0] if "id" in qs_raw.keys() else "")
446
447		self.wfile.write(image if image else b"")
448
449
450if __name__ == "__main__":
451	webServer = HTTPServer((hostName, serverPort), WebServer)
452	print("Server started at http://%s:%s" % (hostName, serverPort))
453
454	try:
455		webServer.serve_forever()
456	except KeyboardInterrupt:
457		pass
458
459	webServer.server_close()
460	print("Server stopped.")