diff --git a/ums/management/db.py b/ums/management/db.py index 4bd8c19..51464c2 100644 --- a/ums/management/db.py +++ b/ums/management/db.py @@ -148,7 +148,7 @@ class DB(): sender=row['sender'], recipient=row['recipient'], time=int(datetime.strptime(row['time'], self._DB_TIME_FORMAT).timestamp()), - message=AgentMessage.model_validate_json(row['json']), + message=AgentMessage.model_validate_json(row['json'], context={"require_file_exists":False}), processed=row['processed'] ) diff --git a/ums/management/interface.py b/ums/management/interface.py index 5fa52a6..0c915ba 100644 --- a/ums/management/interface.py +++ b/ums/management/interface.py @@ -18,7 +18,8 @@ from fastapi.templating import Jinja2Templates from ums.management.db import DB -from ums.utils import AgentMessage +from ums.utils import (AgentMessage, RiddleDataType, + list_shared_data, list_shared_schema, SHARE_PATH) class Interface(): @@ -97,5 +98,9 @@ class Interface(): def new(request: Request): return self.template.TemplateResponse( 'new.html', - {"request" : request, "AgentMessage" : AgentMessage} + {"request" : request, + "AgentMessage" : AgentMessage, "RiddleDataType": RiddleDataType, + "shared_data" : list_shared_data(), "shared_schema" : list_shared_schema(), + "SHARE_PATH" : SHARE_PATH + } ) \ No newline at end of file diff --git a/ums/utils/__init__.py b/ums/utils/__init__.py index 820c931..e5797e0 100644 --- a/ums/utils/__init__.py +++ b/ums/utils/__init__.py @@ -22,4 +22,6 @@ from ums.utils.types import ( from ums.utils.const import * -from ums.utils.request import ManagementRequest \ No newline at end of file +from ums.utils.request import ManagementRequest + +from ums.utils.functions import list_shared_data, list_shared_schema \ No newline at end of file diff --git a/ums/utils/functions.py b/ums/utils/functions.py new file mode 100644 index 0000000..f7d0978 --- /dev/null +++ b/ums/utils/functions.py @@ -0,0 +1,46 @@ +import os + +from typing import List, Callable + +from ums.utils.const import SHARE_PATH + +def list_path(path:str) -> List[str]: + if os.path.isdir(path): + items = [] + for item in os.listdir(path): + full = os.path.join(path, item) + if os.path.isdir(full): + items.extend(list_path(full)) + elif os.path.isfile(full): + items.append(full) + return items + else: + return [] + +def list_shared(filter:Callable=lambda p,n,e: True) -> List[str]: + files = [] + for f in list_path(SHARE_PATH): + r = f[len(SHARE_PATH)+1:] + + if r[0] == '.' or '/.' in r: + # hidden files + continue + + if '/' in r and '.' in r: + path, name, ending = r[:r.rfind('/')], r[r.rfind('/')+1:r.rfind('.')], r[r.rfind('.')+1:] + elif '/' in r: + path, name, ending = r[:r.rfind('/')], r[r.rfind('/')+1:], "" + elif '.' in r: + path, name, ending = "", r[:r.rfind('.')], r[r.rfind('.')+1:] + else: + path, name, ending = "", r, "" + + if filter(path, name, ending): + files.append(r) + return files + +def list_shared_data(): + return list_shared(lambda p,n,e: e != "json") + +def list_shared_schema(): + return list_shared(lambda p,n,e: e == "json") \ No newline at end of file diff --git a/ums/utils/types.py b/ums/utils/types.py index 5808b79..65a55f2 100644 --- a/ums/utils/types.py +++ b/ums/utils/types.py @@ -92,11 +92,15 @@ import os from enum import Enum -from typing import List +from typing import List, Any from typing_extensions import Annotated -from pydantic import BaseModel -from pydantic.functional_validators import AfterValidator +from pydantic import ( + BaseModel, + ValidationError, ValidationInfo, + ValidatorFunctionWrapHandler +) +from pydantic.functional_validators import WrapValidator, AfterValidator from ums.utils.const import SHARE_PATH @@ -127,7 +131,22 @@ def _check_data_file(file_name:str) -> str: return file_name +def _ignore_file_missing( + v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo + ) -> str: + try: + return handler(v) + except ValidationError: + if not info.context is None and \ + "require_file_exists" in info.context and \ + info.context["require_file_exists"] == False and \ + isinstance(v, str): + return "missing:{}".format(v) + else: + raise + class RiddleData(RiddleInformation): + """ A data item to be used to solve the riddle """ @@ -137,7 +156,7 @@ class RiddleData(RiddleInformation): The type of the data item. """ - file_plain: Annotated[str, AfterValidator(_check_data_file)] + file_plain: Annotated[str, AfterValidator(_check_data_file), WrapValidator(_ignore_file_missing)] """ The plain file (as path to file system) without any processing. @@ -145,7 +164,7 @@ class RiddleData(RiddleInformation): The file must exist. """ - file_extracted: Annotated[str, AfterValidator(_check_data_file)] | None = None + file_extracted: Annotated[str, AfterValidator(_check_data_file), WrapValidator(_ignore_file_missing)] | None = None """ The processed files (as path to file system), i.e., a schematic file containing all extracted informations. @@ -153,6 +172,8 @@ class RiddleData(RiddleInformation): The file must exist. """ + + class RiddleSolution(RiddleInformation): """ A solution of a riddle. diff --git a/web/public/static/new.js b/web/public/static/new.js new file mode 100644 index 0000000..5d91dbd --- /dev/null +++ b/web/public/static/new.js @@ -0,0 +1,117 @@ +var fileLast = 0; +function renderFile(cnt, type="", file_extracted="", file_plain=""){ + var template = $("#filesTemplate").html(); + template = template.replaceAll('{cnt}', cnt) + .replaceAll('{file_extracted}', file_extracted) + .replaceAll('{file_extracted}', file_extracted) + .replaceAll('{file_plain}', file_plain); + + ['text', 'image', 'audio'].forEach((t)=>{ + template = template.replaceAll('{'+t+'_selected}', t === type ? 'selected' : '' ); + }); + $("#filesRender").append(template); + + $("button#removeFile"+cnt).click( () => { + $("#filesRow" + cnt).remove(); + update_json_message() + }); + + fileLast = cnt; +} +$("#addFile").click(() => renderFile(++fileLast)); + +function update_json_message(){ + let message = JSON.parse(basic_message); + + var store_values = {} + $(".message-attribute").each((_,v)=>{ + let el = $(v); + let name = el.attr('name'); + let val = el.attr("type") == "checkbox" ? el.prop('checked') : el.val(); + + // optional fields (empty => null) + if( name.endsWith("file_extracted") && val.length == 0 ){ + val = null; + } + + store_values[name] = val; + + let curr_msg = message; + let last_ob = {}; + let last_name = ""; + name.split('.').forEach((e)=>{ + last_ob = curr_msg; + last_name = e; + + if( !curr_msg.hasOwnProperty(e) ){ + curr_msg[e] = {} + } + + curr_msg = curr_msg[e]; + }); + last_ob[last_name] = val; + }); + localStorage.setItem("new_riddle", JSON.stringify(store_values)) + + // fix array for data + if(message.hasOwnProperty('data')){ + var data_items = message['data']; + message['data'] = []; + Object.keys(data_items).forEach((v)=>{ + if( v !== null){ + message['data'].push(data_items[v]); + } + }); + } + + $("#message_content").val(JSON.stringify(message, null, 2)); +} +function load_last_values(){ + if(localStorage.getItem("new_riddle") !== null){ + var items = JSON.parse(localStorage.getItem("new_riddle")) + console.log(items) + Object.keys(items).forEach((k)=>{ + + // if data, create the file select for this id + if(k.startsWith('data.') && $("[name='"+k+"']").length == 0){ + renderFile(parseInt(k.substring(5, k.indexOf('.',5)))) + } + + let el = $("[name='"+k+"']"); + if(el.attr("type") == "checkbox"){ + el.prop('checked', items[k]); + } + else{ + el.val(items[k]); + } + }); + + update_json_message() + } +} +load_last_values(); + +$( document ).on( "change", ".message-attribute", update_json_message) +$( document ).on( "click", ".message-attribute[type=checkbox]", update_json_message) + +function send_json_message(json_str){ + $.ajax( + "/message", + { + contentType : 'application/json', + type : 'POST', + data: json_str, + }, + ).then((d) => { + $("#message_sent .modal-title").text("Message Sent"); + $("#message_sent .modal-body").html('
'+JSON.stringify(d, null, 2)+'
'); + new bootstrap.Modal('#message_sent').show(); + }).fail((d)=>{ + $("#message_sent .modal-title").text("Error Sending Message"); + $("#message_sent .modal-body").html('
'+JSON.stringify(d, null, 2)+'
'); + new bootstrap.Modal('#message_sent').show(); + }); +} +$("#send_message").click( + () => send_json_message($("#message_content").val()) +); \ No newline at end of file diff --git a/web/public/static/main.js b/web/public/static/table.js similarity index 52% rename from web/public/static/main.js rename to web/public/static/table.js index e9d3c10..8446f84 100644 --- a/web/public/static/main.js +++ b/web/public/static/table.js @@ -1,5 +1,3 @@ - - function pagination_link(name, value){ let link_args = db_args; if (name && value){ @@ -35,53 +33,6 @@ if(sessionStorage.hasOwnProperty('auto_refresh') && sessionStorage.getItem('auto enable_auto_refresh() } -function update_json_message(){ - let message = JSON.parse(basic_message); - - var store_values = {} - $(".message-attribute").each((_,v)=>{ - let el = $(v); - let name = el.attr('name'); - let val = el.attr("type") == "checkbox" ? el.prop('checked') : el.val(); - - store_values[name] = val; - - let curr_msg = message; - let last_ob = {}; - let last_name = ""; - name.split('.').forEach((e)=>{ - last_ob = curr_msg; - last_name = e; - curr_msg = curr_msg[e]; - }); - last_ob[last_name] = val; - }); - localStorage.setItem("new_riddle", JSON.stringify(store_values)) - - $("#message_content").val(JSON.stringify(message, null, 2)); -} -function load_last_values(){ - if(localStorage.getItem("new_riddle") !== null){ - var items = JSON.parse(localStorage.getItem("new_riddle")) - Object.keys(items).forEach((k)=>{ - let el = $("[name='"+k+"']"); - if(el.attr("type") == "checkbox"){ - el.prop('checked', items[k]); - } - else{ - el.val(items[k]); - } - }); - - update_json_message() - } -} -if(typeof basic_message != 'undefined'){ - load_last_values(); -} -$(".message-attribute").change(update_json_message); -$(".message-attribute[type=checkbox]").click(update_json_message); - function send_json_message(json_str){ $.ajax( "/message", @@ -94,12 +45,12 @@ function send_json_message(json_str){ $("#message_sent .modal-title").text("Message Sent"); $("#message_sent .modal-body").html('
'+JSON.stringify(d, null, 2)+'
'); new bootstrap.Modal('#message_sent').show(); + }).fail((d)=>{ + $("#message_sent .modal-title").text("Error Sending Message"); + $("#message_sent .modal-body").html('
'+JSON.stringify(d, null, 2)+'
'); + new bootstrap.Modal('#message_sent').show(); }); } -$("#send_message").click( - () => send_json_message($("#message_content").val()) -); - $(".send_message_again").click( (e) => send_json_message( $("pre#row_message_raw_"+$(e.target).attr('idx')).text() ) -); +); \ No newline at end of file diff --git a/web/templates/base.html b/web/templates/base.html index fb3764e..bc316ec 100644 --- a/web/templates/base.html +++ b/web/templates/base.html @@ -43,6 +43,7 @@ {% block maincontent %} {% endblock %} - + {% block morefoot %} + {% endblock %} \ No newline at end of file diff --git a/web/templates/new.html b/web/templates/new.html index 460a92c..adf341e 100644 --- a/web/templates/new.html +++ b/web/templates/new.html @@ -33,6 +33,13 @@ +
+ + +
+ +

Steps

+
@@ -48,6 +55,61 @@
+

Files

+

+ Manually add files to the shared directory and then select them here to assign them to a riddle. + Shared directory is {{SHARE_PATH}} which should be linked to the host using docker to, e.g., ./data/share. +

+ + + {% for f in shared_data %} + + + {% for f in shared_schema %} + + + + +
+
+
+
+ +
+
+

JSON Representation

@@ -67,4 +129,7 @@ +{% endblock %} +{% block morefoot %} + {% endblock %} \ No newline at end of file diff --git a/web/templates/table.html b/web/templates/table.html index c3b16d9..856b34d 100644 --- a/web/templates/table.html +++ b/web/templates/table.html @@ -138,4 +138,7 @@ const db_args = JSON.parse('{{ db_args|tojson }}'); const db_total = {{ db.len(**db_args) }}; +{% endblock %} +{% block morefoot %} + {% endblock %} \ No newline at end of file