Skip to content

Commit 9804158

Browse files
git-lreuterLenhard Reuter
andauthored
Feature/fileupload (#13)
* Define upload folder and allowed extensions * Add file db model * Add file creation function * Add helper functions * Adapt admin-output * Adapt admin-output * Add upload endpoint * Use secure_filename prior to extension check Co-authored-by: Lenhard Reuter <[email protected]>
1 parent 510acfc commit 9804158

File tree

8 files changed

+118
-4
lines changed

8 files changed

+118
-4
lines changed

learners/conf/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ def __init__(self):
124124
"login_text": learners_config.get("learners").get("login_text"),
125125
}
126126

127+
self.allowed_extensions = learners_config.get("learners").get("upload_extensions")
128+
self.upload_folder = learners_config.get("learners").get("upload_folder")
129+
127130

128131
def build_config(app):
129132

learners/conf/config_schema.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
"login_text",
2424
default="In order to participate in the exercises, please log in with your credentials. You will then get access to the documentation and the introduction to the tools to be used, the Exercises-Control and get access to the VNC client from which the exercises can be performed.",
2525
): Str(),
26+
Optional("upload_folder", default="/var/tmp/"): Str(),
27+
Optional("upload_extensions", default=["txt", "pdf", "png", "jpg", "jpeg", "gif", "json", "svg"]): Seq(Str()),
2628
}
2729
),
2830
"jwt": Map(

learners/conf/db_models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ class Execution(db.Model):
2626
exercise_id = db.Column(db.Integer, db.ForeignKey("exercise.id"), nullable=False)
2727

2828

29+
class Attachment(db.Model):
30+
id = db.Column(db.Integer, primary_key=True)
31+
filename = db.Column(db.String(120), nullable=False)
32+
filename_hash = db.Column(db.String(120), nullable=False)
33+
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
34+
35+
2936
class Exercise(db.Model):
3037
id = db.Column(db.Integer, primary_key=True)
3138
type = db.Column(db.String(120), nullable=False)

learners/functions/database.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import hashlib
12
import json
23
from datetime import datetime, timezone
34
from sqlite3 import IntegrityError
45
from typing import Tuple
56

67
from learners import logger
78
from learners.conf.config import cfg
8-
from learners.conf.db_models import Execution, Exercise, User
9+
from learners.conf.db_models import Attachment, Execution, Exercise, User
910
from learners.database import db
1011
from learners.functions.helpers import extract_exercises
1112
from sqlalchemy import event, nullsfirst
@@ -179,3 +180,26 @@ def get_completed_state(user_id: int, exercise_id: int) -> dict:
179180
except Exception as e:
180181
logger.exception(e)
181182
return None
183+
184+
185+
def db_create_file(filename: str, username: str) -> str:
186+
try:
187+
user_id = User.query.filter_by(name=username).first().id
188+
filename_hash = hashlib.md5(filename.encode("utf-8")).hexdigest()
189+
file = Attachment(filename=filename, filename_hash=filename_hash, user_id=user_id)
190+
db.session.add(file)
191+
db.session.commit()
192+
return filename_hash
193+
194+
except Exception as e:
195+
logger.exception(e)
196+
return None
197+
198+
199+
def get_filename_from_hash(filename_hash):
200+
try:
201+
session = db.session.query(Attachment).filter_by(filename_hash=filename_hash).first()
202+
return session.filename
203+
except Exception as e:
204+
logger.exception(e)
205+
return None

learners/functions/helpers.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,22 @@ def append_or_update_subexercise(parent_exercise: dict, child_exercise: dict) ->
7979
parent_exercise["exercises"].append(child_exercise)
8080

8181
return parent_exercise
82+
83+
84+
def allowed_file(filename):
85+
return "." in filename and filename.rsplit(".", 1)[1].lower() in cfg.allowed_extensions
86+
87+
88+
def replace_attachhment_with_url(formData):
89+
from learners.functions.database import get_filename_from_hash
90+
91+
for key, value in formData.items():
92+
if key == "attachment":
93+
filename = get_filename_from_hash(value)
94+
hyperlink = f"/upload/{filename}"
95+
formData[key] = hyperlink
96+
if isinstance(value, dict):
97+
formData[key] = replace_attachhment_with_url(value)
98+
continue
99+
100+
return formData

learners/routes/admin.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
get_exercise_by_name,
1010
get_user_by_id,
1111
)
12-
from learners.functions.helpers import extract_history
12+
from learners.functions.helpers import extract_history, replace_attachhment_with_url
1313
from learners.functions.results import construct_results_table
1414
from learners.jwt_manager import admin_required
1515
from learners.logger import logger
@@ -53,6 +53,7 @@ def get_exercise_result(user_id, exercise_name):
5353

5454
if exercise.type == "form":
5555
data["form"] = json.loads(last_execution.form_data) if last_execution else None
56+
data["form"] = replace_attachhment_with_url(data["form"])
5657

5758
data["history"] = extract_history(executions) if executions else None
5859

learners/routes/execution.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import json
2+
import os
23
import uuid
34

4-
from flask import Blueprint, jsonify, request
5+
from flask import Blueprint, jsonify, request, send_from_directory
56
from flask_jwt_extended import get_jwt_identity, jwt_required
67
from learners import logger
78
from learners.functions.database import (
89
db_create_execution,
10+
db_create_file,
911
get_completed_state,
1012
get_current_executions,
1113
get_exercise_by_name,
@@ -14,7 +16,12 @@
1416
get_user_by_name,
1517
)
1618
from learners.functions.execution import call_venjix, send_form_via_mail, update_execution_response, wait_for_response
17-
from learners.functions.helpers import append_key_to_dict, append_or_update_subexercise
19+
from learners.functions.helpers import allowed_file, append_key_to_dict, append_or_update_subexercise
20+
21+
22+
from werkzeug.utils import secure_filename
23+
from learners.conf.config import cfg
24+
1825

1926
execution_api = Blueprint("execution_api", __name__)
2027

@@ -29,6 +36,7 @@ def run_execution(type):
2936
response = {"uuid": execution_uuid, "connected": False, "executed": False}
3037

3138
data = request.get_json()
39+
3240
if db_create_execution(type, data, username, execution_uuid):
3341
if type == "script":
3442
response["connected"], response["executed"] = call_venjix(username, data["script"], execution_uuid)
@@ -96,3 +104,51 @@ def get_execution_state():
96104
results[parent] = append_or_update_subexercise(results[parent], exerciseobj)
97105

98106
return jsonify(success_list=results)
107+
108+
109+
@execution_api.route("/upload", methods=["POST"])
110+
@jwt_required(locations="headers")
111+
def upload_file():
112+
113+
response = {
114+
"completed": False,
115+
"executed": False,
116+
"msg": None,
117+
"file": None,
118+
}
119+
120+
if "file" not in request.files:
121+
response["msg"] = "file missing"
122+
return jsonify(response)
123+
124+
file = request.files["file"]
125+
126+
if file.filename == "":
127+
response["msg"] = "no file selected"
128+
return jsonify(response)
129+
130+
username = get_jwt_identity()
131+
132+
if file:
133+
filename = f"{username}_{secure_filename(file.filename)}"
134+
if not allowed_file(filename):
135+
response["msg"] = "file type not allowed"
136+
return jsonify(response)
137+
try:
138+
file.save(os.path.join(cfg.upload_folder, filename))
139+
filehash = db_create_file(filename, get_jwt_identity())
140+
response["completed"] = True
141+
response["executed"] = True
142+
response["msg"] = "file sucessfully uploaded"
143+
response["file"] = filehash
144+
145+
except Exception as e:
146+
response["msg"] = "server error"
147+
148+
return jsonify(response)
149+
150+
151+
@execution_api.route("/upload/<name>")
152+
@jwt_required()
153+
def download_file(name):
154+
return send_from_directory(cfg.upload_folder, name)

learners/templates/result_details.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ <h5>{{ key }}</h5>
4848
<span class="detail {% if subvalue == True %}true{% elif subvalue == False %}false{% endif %}">
4949
{% if subvalue != "" and (subvalue | string).startswith('http') %}
5050
<a href="{{subvalue}}" target="_blank">{{subvalue | truncate(150, True, ' ...') }}</a>
51+
{% elif subvalue != "" and (subvalue | string).startswith('/upload/') %}
52+
<a href="{{subvalue}}" target="_blank">download attachment</a>
5153
{% elif subvalue != "" %}
5254
{{subvalue}}
5355
{% else %}

0 commit comments

Comments
 (0)