From db2fb91b548ddb756f81d0ec7e7154632d5e183f Mon Sep 17 00:00:00 2001 From: Lennart Franken <lennart@franken-familie.de> Date: Tue, 16 Aug 2022 21:22:56 +0200 Subject: [PATCH 1/2] Added notification & activity Log app --- code/app/database.py | 18 ++-- code/app/sessionhelper.py | 5 +- code/apps/activityLog/code/mainapp.py | 21 +++++ code/apps/activityLog/code/render.py | 18 ++++ code/apps/activityLog/code/reutil.py | 32 +++++++ code/apps/activityLog/meta.json | 9 ++ code/apps/activityLog/static/activity.css | 3 + code/apps/activityLog/translations/de.json | 4 + code/apps/activityLog/translations/en.json | 4 + code/apps/adminarea/code/mainapp.py | 9 ++ code/apps/adminarea/code/notification.py | 10 +++ code/apps/adminarea/translations/de.json | 3 + code/apps/adminarea/translations/en.json | 3 + code/apps/lists/code/mainapp.py | 32 ++++++- code/apps/lists/code/notification.py | 25 ++++++ code/apps/lists/code/reutil.py | 12 +-- code/apps/lists/static/default.css | 24 ----- code/apps/lists/translations/de.json | 21 ++++- code/apps/lists/translations/en.json | 21 ++++- code/apps/mail/code/mail.py | 6 +- code/apps/mail/code/send.py | 14 ++- code/apps/mail/translations/de.json | 3 +- code/apps/mail/translations/en.json | 3 +- code/apps/notifications/code/mainapp.py | 41 ++++++++- code/apps/notifications/code/render.py | 44 +++++++++ code/apps/notifications/code/reutil.py | 95 ++++++++++++++++++-- code/apps/notifications/static/default.css | 9 ++ code/apps/notifications/static/default.js | 20 +++++ code/apps/notifications/translations/de.json | 12 ++- code/apps/notifications/translations/en.json | 11 +++ code/apps/profile/code/render.py | 6 +- code/apps/root/meta.json | 2 +- code/apps/root/static/default.css | 28 +++++- 33 files changed, 503 insertions(+), 65 deletions(-) create mode 100644 code/apps/activityLog/code/mainapp.py create mode 100644 code/apps/activityLog/code/render.py create mode 100644 code/apps/activityLog/code/reutil.py create mode 100644 code/apps/activityLog/meta.json create mode 100644 code/apps/activityLog/static/activity.css create mode 100644 code/apps/activityLog/translations/de.json create mode 100644 code/apps/activityLog/translations/en.json create mode 100644 code/apps/adminarea/code/notification.py create mode 100644 code/apps/lists/code/notification.py create mode 100644 code/apps/notifications/code/render.py create mode 100644 code/apps/notifications/static/default.css create mode 100644 code/apps/notifications/static/default.js create mode 100644 code/apps/notifications/translations/en.json diff --git a/code/app/database.py b/code/app/database.py index eead4bc..1dcf4ef 100644 --- a/code/app/database.py +++ b/code/app/database.py @@ -4,6 +4,8 @@ import util, errors, config, event import sessionhelper as r +notification = util.import_app_module("adminarea", "notification") + class database: def __init__(self, datafolder, scriptpath): self.datafolder = datafolder+"/" @@ -68,11 +70,11 @@ class database: self.logToUsermessage(userid, category, message) def logToUsermessage(self, userid, category, message): - if "adminarea" in self.apps: - for user_id in self.users: - user = self.users[user_id] - if user.appPermissions.get("adminarea") == "admin": - user.addUsermessage(ntcenter={"content":r.link(visibleas=f"[{category}] {r.username(self, userid)}: ", linkto="/adminarea/log")+message[:300], "classes":["logusermessage"]}) + for user_id in self.users: + user = self.users[user_id] + if user.appPermissions.get("adminarea") == "admin": + notification.triggerNewLogEntrie(user, self, userid, category, message) + def logSave(self): util.write(self.logfile, self.logs) @@ -226,11 +228,13 @@ class User: def log(self, category, message): self.database.log(userid=self.id, category=category, message=message) - def addUsermessage(self, event=None, ntcenter=None, email=None): + def addUsermessage(self, ntcenter=None, email=None, activityLog=None): id = None if ntcenter: # notification center id = util.generate_token() self.usermessages[id] = ntcenter if email and "mail" in self.database.enabledApps(): - self.database.apps["mail"].code.send.sendEmail(self, email["subject"], email["content"], email.get("overwriteVerify") == True) + self.database.apps["mail"].code.send.sendEmail(self, email["subject"], email["content"], email.get("overwriteVerify") == True, pack=email.get("pack")!=False, linkto = email.get("linkto"), blockFurtherMessages=email.get("blockFurtherMessages")) + if activityLog: + self.activity.push(activityLog["appid"], activityLog["eventname"], activityLog["message"], linkto=activityLog.get("linkto")) return(id) diff --git a/code/app/sessionhelper.py b/code/app/sessionhelper.py index 054a080..a8ea5ee 100644 --- a/code/app/sessionhelper.py +++ b/code/app/sessionhelper.py @@ -66,6 +66,9 @@ def button(visibleas="", name=None, value=None, id=None, onclick=None, classes=[ connectedInputFieldScript = "" return(f"""<button {_createattibutesstr(attributes)}>{visibleas}</button>{connectedInputFieldScript}""") +def createcheckboxButton(checked = False, onchange="", otherattribues=[], name="", id=None): + return(button(nobutton = True, visibleas = ".", classes = ["checkbox"] + (["checked"] if checked else []), onclick=onchange, otherattribues=otherattribues, name=name, id=id)) + def link(visibleas="", classes=[], style=None, linkto=None, isbutton=False, pressable=True, target=None): if isbutton: if pressable: @@ -240,7 +243,7 @@ def createtable(headers=[], rows=[], classes=[], id=None): if type(item) == str: row_html += f"<td>{item}</td>" else: - row_html += f"<td {_createclassesstr(item['classes'])}>{item['str']}</td>" + row_html += f"<td {_createclassesstr(item['classes'])} colspan = '{item['colspan'] if 'colspan' in item else 1}'>{item['str']}</td>" row_html += "</tr>" rows_html += row_html return(f"<table {_createclassesstr(classes)} {('id = ' + id) if id else ''}>{headers_html}{rows_html}</table>") diff --git a/code/apps/activityLog/code/mainapp.py b/code/apps/activityLog/code/mainapp.py new file mode 100644 index 0000000..73202bb --- /dev/null +++ b/code/apps/activityLog/code/mainapp.py @@ -0,0 +1,21 @@ +import util + +reutil = util.import_app_module("activityLog", "reutil") +render = util.import_app_module("activityLog", "render") + +def init(data): + for userid in data.users: + user = data.getUser(id=userid) + user.activity = reutil.activity(user) + +def onNewuser(user): + user.activity = reutil.activity(user) + +def activityLog(user, data, path, modules): + return(render.renderActivity(user)) + +def activityLogPacked(user, data, path, modules): + return({"type":"packed", "content":activityLog(user, data, path, modules), "css":["activityLog/activity.css"]}) + + +redirects={"/activityLog":{"directto":activityLogPacked, "type":"nosession", "require_login":True}} diff --git a/code/apps/activityLog/code/render.py b/code/apps/activityLog/code/render.py new file mode 100644 index 0000000..97ffbad --- /dev/null +++ b/code/apps/activityLog/code/render.py @@ -0,0 +1,18 @@ +import sessionhelper as r + +def renderActivity(user): + result = r.heading(3, "{translation:activityLog/title}") + if user.activity.getEntries(): + for entrie in user.activity.getEntries()[::-1]: + result += renderEntrie(user, entrie) + else: + result += r.center(r.p("{translation:activityLog/noEntriesYet}")) + result += r.center(r.link(visibleas="{translation:notifications/changeNotifications}", linkto="/notifications", isbutton=True)) + return(result) + +def renderEntrie(user, entrie): + fieldset = r.createfieldset( + legend="{translation:"+entrie["app"]+"/title}: " + entrie["eventname"], + content=r.p(r.date(unix=entrie["time"]), tag="div", classes=["floatDate"]) + r.p(entrie["message"], tag="div") + ) + return(r.link(visibleas=fieldset, linkto=entrie["linkto"], classes=["nolink"]) if entrie["linkto"] else fieldset) diff --git a/code/apps/activityLog/code/reutil.py b/code/apps/activityLog/code/reutil.py new file mode 100644 index 0000000..ae38903 --- /dev/null +++ b/code/apps/activityLog/code/reutil.py @@ -0,0 +1,32 @@ +import util, time + +class activity: + def __init__(self, user): + self.user = user + self.data = self.user.database + self.logpath = self.data.apps["activityLog"].userdatapath(user=user)+"log.json" + + try: + self.entries = util.read(self.logpath, importjson=True) + except FileNotFoundError: + self.entries = [] + self.saveEntries() + + def saveEntries(self): + util.write(self.logpath, self.entries) + + def push(self, appid, eventname, message, linkto=None): + self.entries.append({ + "app":appid, + "time":time.time(), + "message":message, + "eventname":eventname, + "linkto":linkto + }) + self.saveEntries() + + def getEntries(self): + while self.entries and self.entries[0]["time"] + 86400*7 < time.time(): + self.entries.pop(0) + self.saveEntries() + return(self.entries) diff --git a/code/apps/activityLog/meta.json b/code/apps/activityLog/meta.json new file mode 100644 index 0000000..afad55a --- /dev/null +++ b/code/apps/activityLog/meta.json @@ -0,0 +1,9 @@ +{ + "version":"0.0.0", + "id":"activityLog", + "showinheader":[ + ["{translation:activityLog/title}", "/activityLog", -9] + ], + "showinfooter":[], + "showapprights":false +} diff --git a/code/apps/activityLog/static/activity.css b/code/apps/activityLog/static/activity.css new file mode 100644 index 0000000..49fa6c7 --- /dev/null +++ b/code/apps/activityLog/static/activity.css @@ -0,0 +1,3 @@ +.floatDate{ + float: right; +} diff --git a/code/apps/activityLog/translations/de.json b/code/apps/activityLog/translations/de.json new file mode 100644 index 0000000..9c3ef0e --- /dev/null +++ b/code/apps/activityLog/translations/de.json @@ -0,0 +1,4 @@ +{ + "title":"Aktivität", + "noEntriesYet":"Bisher keine Benachrichtungen" +} diff --git a/code/apps/activityLog/translations/en.json b/code/apps/activityLog/translations/en.json new file mode 100644 index 0000000..bc7378c --- /dev/null +++ b/code/apps/activityLog/translations/en.json @@ -0,0 +1,4 @@ +{ + "title":"Activity", + "noEntriesYet":"No recent notifications" +} diff --git a/code/apps/adminarea/code/mainapp.py b/code/apps/adminarea/code/mainapp.py index c6beb2b..a216bf7 100644 --- a/code/apps/adminarea/code/mainapp.py +++ b/code/apps/adminarea/code/mainapp.py @@ -6,6 +6,15 @@ import sessionhelper as r api = util.import_app_module("adminarea", "api") reutil = util.import_app_module("adminarea", "reutil") +notify = util.import_app_module("notifications", "reutil") + +def getNotificicationEvents(user): + result = [] + if user.appPermissions["adminarea"] == "admin": + result.append(notify.notificationEvent(user, "adminarea", "newLogEntrie")) + return(result) + + def restart_server(): threading.Thread(target=restart_server_run).start() def restart_server_run(): diff --git a/code/apps/adminarea/code/notification.py b/code/apps/adminarea/code/notification.py new file mode 100644 index 0000000..c9c7318 --- /dev/null +++ b/code/apps/adminarea/code/notification.py @@ -0,0 +1,10 @@ +import sessionhelper as r +import util + +notify = util.import_app_module("notifications", "reutil") + +def triggerNewLogEntrie(user, data, userid, category, message): + ntcenter = {"content":r.link(visibleas=f"[{category}] {r.username(data, userid)}: ", linkto="/adminarea/log")+message[:300], "classes":["logusermessage"]} + activityLog = r.p(f"[{category}] {r.username(data, userid)}:", tag="span", style="font-weight: bold")+r.br+message + email = {"subject":"{translation:adminarea/notificationEvents/newLogEntrie}", "content":r.link(visibleas=f"[{category}] {r.username(data, userid)}: ", linkto=data.meta["domain"]+"/adminarea/log")+r.br+message} + notify.triggerEvent(user=user, appid="adminarea", eventid = "newLogEntrie", ntcenter=ntcenter, email=email, activityLog=activityLog, linkto="/adminarea/log/"+user.database.logfile.split("/")[-1]) diff --git a/code/apps/adminarea/translations/de.json b/code/apps/adminarea/translations/de.json index cd15684..0df7fda 100644 --- a/code/apps/adminarea/translations/de.json +++ b/code/apps/adminarea/translations/de.json @@ -113,5 +113,8 @@ "logs":"Logs", "path":"Speicherort", "linkdomain":"Adresse für Links (z.B. http://localhost:8080)" + }, + "notificationEvents":{ + "newLogEntrie":"Neuer Log Eintrag" } } diff --git a/code/apps/adminarea/translations/en.json b/code/apps/adminarea/translations/en.json index a6b9435..d9f1e4e 100644 --- a/code/apps/adminarea/translations/en.json +++ b/code/apps/adminarea/translations/en.json @@ -113,5 +113,8 @@ "logs":"Logs", "path":"Storage location", "linkdomain":"Address for links (e.g. http://localhost:8080)" + }, + "notificationEvents":{ + "newLogEntrie":"New log entrie" } } diff --git a/code/apps/lists/code/mainapp.py b/code/apps/lists/code/mainapp.py index 19f58b9..2172f75 100644 --- a/code/apps/lists/code/mainapp.py +++ b/code/apps/lists/code/mainapp.py @@ -5,6 +5,23 @@ from flask import request reutil = util.import_app_module("lists", "reutil") render = util.import_app_module("lists", "render") latex = util.import_app_module("lists", "latex") +notification = util.import_app_module("lists", "notification") + +notify = util.import_app_module("notifications", "reutil") + + +def getNotificicationEvents(user): + events = [ + notify.notificationEvent(user, "lists", "newListShared"), + notify.notificationEvent(user, "lists", "listDeleted") + ] + listChanged, groups = [], [] + for list_obj_id in reutil.getListsWithAccess(user.database, user.id): + list_obj = reutil.checklist(list_obj_id, user.database) + listChanged.append(notify.notificationEvent(user, "lists", "listChanged?"+list_obj.fullid, list_obj.meta["name"], settingsfile=list_obj.path+"notify/"+user.id+".json", timeBlocked=600)) + if listChanged: + events.append(notify.notificationEventGroup(user, "lists", "listChanged", events=listChanged)) + return(events) def backtostart(): return(r.unpackedRedirect("/lists")) @@ -90,8 +107,9 @@ def lists(user, data, path, modules): if "deleteList" in request.form: list_obj = getList(data, user, request.form.get("deleteList")) if list_obj: - shutil.rmtree(list_obj.path) + notification.triggerListDeleted(user, list_obj) user.addUsermessage(ntcenter={"content":"{translation:lists/usermessages/deletedList;name=%s}" % (list_obj.meta["name"])}) + shutil.rmtree(list_obj.path) return(r.unpackedRedirect("/lists")) if "saveDescription" in path: @@ -99,6 +117,7 @@ def lists(user, data, path, modules): htmlid, id = json_data["id"].split("/", 1) list_obj = getList(data, user, id) if list_obj: + notification.triggerListEdited(user, list_obj) list_obj.meta["description"] = r.tohtml(r.indentMobileMarkdown(str(json_data.get("newtext")))) list_obj.saveMeta() return({"type":"unpacked", "content":"success"}) @@ -107,6 +126,7 @@ def lists(user, data, path, modules): userid, listid, newName = request.form.get("renameList").split("/") list_obj = getList(data, user, f"{userid}/{listid}") if list_obj: + notification.triggerListEdited(user, list_obj) list_obj.meta["name"] = r.tohtml(newName) list_obj.saveMeta() return(r.unpackedRedirect(list_obj.linkto)) @@ -121,6 +141,7 @@ def lists(user, data, path, modules): user.addUsermessage(ntcenter={"content":"{translation:lists/usermessages/releaseAlreadyShared;name=%s}" % (newUser.username), "classes":["warning"], "requireRemove":True}) else: list_obj.addSharing(newUser) + notification.triggerNewRelease(newUser, list_obj, user) else: user.addUsermessage(ntcenter={"content":"{translation:lists/usermessages/releaseUserNotExists;name=%s}" % (r.tohtml(newUser_name)), "classes":["warning"], "requireRemove":True}) return({"type":"unpacked", "content":render.renderShared(data, user, list_obj), "translate":True}) @@ -148,6 +169,7 @@ def lists(user, data, path, modules): if fullid in list_obj.getAppendedLists() or fullid in list_obj.getAppendedLists(direction="to"): user.addUsermessage(ntcenter={"content":"{translation:lists/usermessages/alreadyAppended}", "requireRemove":True, "classes":["warning"]}) else: + notification.triggerListEdited(user, list_obj) list_obj.appendList(fullid) break if not found: @@ -160,6 +182,7 @@ def lists(user, data, path, modules): list_obj2 = getList(data, user, f"{removeuserid}/{removelistid}") if list_obj and list_obj2: if list_obj2.fullid in list_obj.getAppendedLists(recursive=False): + notification.triggerListEdited(user, list_obj) list_obj.removeAppend(list_obj2.fullid) user.addUsermessage(ntcenter={"content":"{translation:lists/usermessages/removedAppend}"}) return({"type":"unpacked", "content":render.renderAppend(data, user, list_obj), "translate":True}) @@ -172,6 +195,7 @@ def lists(user, data, path, modules): while newItem.startswith(" "): indent += 1 newItem = newItem.replace(" ", "", 1) + notification.triggerListEdited(user, list_obj) list_obj.addEntrie(newItem, indent) return({"content":render.renderItems(data, user, list_obj), "type":"unpacked", "translate":True}) @@ -179,6 +203,7 @@ def lists(user, data, path, modules): userid, listid, entrieid, viewuserid, viewlistid, checked_str = ajaxdata(5) list_obj, entrie = getEntrie(data, user, f"{userid}/{listid}/{entrieid}") if entrie: + notification.triggerListEdited(user, list_obj) entrie.checked = checked_str == "True" entrie.save() else: @@ -189,6 +214,7 @@ def lists(user, data, path, modules): userid, listid, entrieid, viewuserid, viewlistid = ajaxdata(4) list_obj, entrie = getEntrie(data, user, f"{userid}/{listid}/{entrieid}") if entrie: + notification.triggerListEdited(user, list_obj) entrie.delete() else: entrieNotFound(user) @@ -199,6 +225,7 @@ def lists(user, data, path, modules): listitem, userid, listid, entrieid = json_data["id"].split("/") list_obj, entrie = getEntrie(data, user, f"{userid}/{listid}/{entrieid}") if entrie: + notification.triggerListEdited(user, list_obj) entrie.value = r.tohtml(json_data["newtext"]) entrie.save() return({"content":"success", "type":"unpacked", "translate":False}) @@ -219,6 +246,8 @@ def lists(user, data, path, modules): error = list_obj.clearChecked(user) if error: entrieNotFound(user) + else: + notification.triggerListEdited(user, list_obj) return({"content":render.renderItems(data, user, list_obj), "type":"unpacked", "translate":True}) if "setarchived" in path: @@ -234,6 +263,7 @@ def lists(user, data, path, modules): targetlist, targetentrie = getEntrie(data, user, f"{targetuserid}/{targetlistid}/{targetentrieid}") sourcelist, sourceentrie = getEntrie(data, user, f"{sourceuserid}/{sourcelistid}/{sourceentrieid}") if targetlist.fullid == sourcelist.fullid and targetentrie and sourceentrie: + notification.triggerListEdited(user, list_obj) if targetentrie.id == sourceentrie.id: if indent in ["right", "left"]: targetlist.setIndent(targetentrie, targetentrie.indent + (-1 if indent == "left" else 1)) diff --git a/code/apps/lists/code/notification.py b/code/apps/lists/code/notification.py new file mode 100644 index 0000000..2242336 --- /dev/null +++ b/code/apps/lists/code/notification.py @@ -0,0 +1,25 @@ +import util +notify = util.import_app_module("notifications", "reutil") + +def triggerNewRelease(user, list_obj, userShared): + text = "{translation:lists/notifications/newrelease/text;listname=%s;userSharedName=%s}" % (list_obj.meta["name"], userShared.username) + ntcenter = {"content":text} + activityLog = text + email = {"subject":"{translation:lists/notifications/newrelease/subject}", "content":text} + notify.triggerEvent(user=user, appid="lists", eventid = "newListShared", ntcenter=ntcenter, email=email, activityLog=activityLog, linkto=user.database.getDomain()+list_obj.linkto) + +def triggerListDeleted(userActing, list_obj): + text = "{translation:lists/notifications/listDeleted/text;listname=%s;userActing=%s}" % (list_obj.meta["name"], userActing.username) + ntcenter = {"content":text} + activityLog = text + email = {"subject":"{translation:lists/notifications/listDeleted/subject}", "content":text} + for user in notify.filterOutUser(userActing, list_obj.getUsersWithAccess()): + notify.triggerEvent(user=user, appid="lists", eventid = "listDeleted", ntcenter=ntcenter, email=email, activityLog=activityLog) + +def triggerListEdited(userActing, list_obj): + text = "{translation:lists/notifications/listEdited/text;listname=%s;userActing=%s}" % (list_obj.meta["name"], userActing.username) + ntcenter = {"content":text} + activityLog = text + email = {"subject":"{translation:lists/notifications/listEdited/subject;listname=%s}" % list_obj.meta["name"], "content":text} + for user in notify.filterOutUser(userActing, list_obj.getUsersWithAccess()): + notify.triggerEvent(user=user, appid="lists", eventid = "listChanged?"+list_obj.fullid, ntcenter=ntcenter, email=email, activityLog=activityLog) diff --git a/code/apps/lists/code/reutil.py b/code/apps/lists/code/reutil.py index 9556b0f..768ccba 100644 --- a/code/apps/lists/code/reutil.py +++ b/code/apps/lists/code/reutil.py @@ -90,6 +90,9 @@ class checklist: return(shares["toothers"][self.id]) return([]) + def getUsersWithAccess(self): + return (self.getSharedTo()+[self.userid]) + def addSharing(self, newUser): shares = getShared(self.data, self.userid) if not self.id in shares["toothers"]: @@ -219,10 +222,10 @@ class entrie: def render(self, viewfullid, checked): return(r.p( tag="div", - content=createcheckboxButton(checked=self.checked, onchange=f"return checkitem('{not self.checked}', this.name)", name=f"{self.fullid}/{viewfullid}", otherattribues=["data-indent = 'left' "]) + + content=r.createcheckboxButton(checked=self.checked, onchange=f"return checkitem('{not self.checked}', this.name)", name=f"{self.fullid}/{viewfullid}", otherattribues=["data-indent = 'left' "]) + createReorder() + r.p(tag="div", content=self.value, classes=["editItem", "autosaveTextarea", "autosaveData", "inputFieldPreventSubmit"], id=f"listitem/{self.fullid}", otherattribues=["role='textbox'", "contenteditable= 'true'", "data-address='lists/editItem'", "data-indent='right'"]) + - r.button(visibleas="", classes=["deleteEntrieButton"], nobutton=True, confirm="{translation:lists/list/items/confirmdelete;name=%s}" % r.clearForTranslation(self.value), onclick=f"return deleteitem('{self.fullid}/{viewfullid}')"), + r.button(visibleas="", classes=["deleteEntrieButton", "backgroudImageButton"], nobutton=True, confirm="{translation:lists/list/items/confirmdelete;name=%s}" % r.clearForTranslation(self.value), onclick=f"return deleteitem('{self.fullid}/{viewfullid}')"), classes=["item"] + (["checked"] if (self.checked or checked) else []), style=f"margin-left: {self.indent}em", id = util.generate_token() @@ -244,8 +247,5 @@ class entrie: def __repr__(self): return(f"{self.value}/{self.checked}") -def createcheckboxButton(checked = False, onchange="", otherattribues=[], name=""): - return(r.button(nobutton = True, visibleas = ".", classes = ["checkbox"] + (["checked"] if checked else []), onclick=onchange, otherattribues=otherattribues, name=name)) - def createReorder(): - return(r.button(nobutton=True, visibleas="." ,classes = ["reorder"], onclick="return false", otherattribues=["draggable = 'true' "])) + return(r.button(nobutton=True, visibleas="." ,classes = ["reorder", "backgroudImageButton"], onclick="return false", otherattribues=["draggable = 'true' "])) diff --git a/code/apps/lists/static/default.css b/code/apps/lists/static/default.css index 6bcae65..cbf8b98 100644 --- a/code/apps/lists/static/default.css +++ b/code/apps/lists/static/default.css @@ -37,36 +37,12 @@ div.item.checked div{ text-decoration: line-through; } -.checkbox, .deleteEntrieButton, .reorder{ - height: 1em; - min-width: 0em; - min-height: 0em; - width: 1em; - background-size: contain; - background-repeat: no-repeat; - background-position: center; - display: inline-block; - color: rgba(0,0,0,0) -} - .reorder{ background-image: url("/static/startid/root/drag.png"); position: relative; top: 0.15em; margin-right: 0.5em; } -.checkbox{ - border: 0.1em solid black; - border-radius: 0.4em; - background-color: rgb(200,200,200); - margin-right: 0.5em; - position: relative; - top: 0.25em; -} -.checkbox.checked{ - background-color: blue; - background-image: url("/static/startid/root/checkbox.png"); -} .item .deleteEntrieButton{ margin-left: 0.5em; diff --git a/code/apps/lists/translations/de.json b/code/apps/lists/translations/de.json index cde19d6..1d65bb3 100644 --- a/code/apps/lists/translations/de.json +++ b/code/apps/lists/translations/de.json @@ -76,5 +76,24 @@ "export":{ "state":"Stand: {date}" }, - "unusedNameToCreateNew":"Um eine neue Liste zu erstellen, gib einfach einen nicht vergebenen Namen ein." + "unusedNameToCreateNew":"Um eine neue Liste zu erstellen, gib einfach einen nicht vergebenen Namen ein.", + "notificationEvents":{ + "newListShared":"Liste wird mit dir geteilt", + "listDeleted":"Liste wird gelöscht", + "listChanged":"Liste wird bearbeitet" + }, + "notifications":{ + "newrelease":{ + "text":"{userSharedName} hat die Liste {listname} mit dir geteilt.", + "subject":"Eine Liste wurde mit dir geteilt" + }, + "listDeleted":{ + "text":"{userActing} hat die Liste {listname} gelöscht.", + "subject":"Eine Liste wurde gelöscht" + }, + "listEdited":{ + "text":"{userActing} hat die Liste {listname} geändert.", + "subject":"{listname} wurde geändert" + } + } } diff --git a/code/apps/lists/translations/en.json b/code/apps/lists/translations/en.json index be73cea..f303ecd 100644 --- a/code/apps/lists/translations/en.json +++ b/code/apps/lists/translations/en.json @@ -76,5 +76,24 @@ "export":{ "state":"State: {date}" }, - "unusedNameToCreateNew":"To create a new list, simply enter an unused name." + "unusedNameToCreateNew":"To create a new list, simply enter an unused name.", + "notificationEvents":{ + "newListShared":"List is shared with you", + "listDeleted":"Liste is deleted", + "listChanged":"Liste is edited" + }, + "notifications":{ + "newrelease":{ + "text":"{userSharedName} has shared {listname} with you.", + "subject":"A list has been shared with you" + }, + "listDeleted":{ + "text":"{userActing} deleted {listname}", + "subject":"A list has been deleted" + }, + "listEdited":{ + "text":"{userActing} has changed {listname}", + "subject":"{listname} has been changed" + } + } } diff --git a/code/apps/mail/code/mail.py b/code/apps/mail/code/mail.py index de4aa2d..644778b 100644 --- a/code/apps/mail/code/mail.py +++ b/code/apps/mail/code/mail.py @@ -1,13 +1,11 @@ import sessionhelper as r def VerificationEmail(user): - result = r.p("{translation:mail/hello;name=%s}" % user.username) link = user.database.getDomain()+"/verifyEmail/"+user.emailAddress["tokenToVerify"] - result += r.p("{translation:mail/newEmail/verificationEmailLink}: " + r.link(visibleas = link, linkto = link)) + result = r.p("{translation:mail/newEmail/verificationEmailLink}: " + r.link(visibleas = link, linkto = link)) return({"subject":"{translation:mail/newEmail/verificationEmailSubject}", "content":result, "overwriteVerify":True}) def ResetPassword(user): - result = r.p("{translation:mail/hello;name=%s}" % user.username) link = user.database.getDomain()+"/resetPassword/"+user.id + "/" + user.passwordResetToken - result += r.p("{translation:mail/resetPassword/emailContent}: " + r.link(visibleas = link, linkto = link)) + result = r.p("{translation:mail/resetPassword/emailContent}: " + r.link(visibleas = link, linkto = link)) return({"subject":"{translation:mail/resetPassword/emailSubject}", "content":result}) diff --git a/code/apps/mail/code/send.py b/code/apps/mail/code/send.py index 8400c0e..38963c3 100644 --- a/code/apps/mail/code/send.py +++ b/code/apps/mail/code/send.py @@ -8,10 +8,10 @@ from threading import Thread reutil = util.import_app_module("mail", "reutil") -def sendEmail(user, subject, content, overwriteVerify=False): - Thread(target=sendEmailSend, args = (user, subject, content, overwriteVerify)).start() +def sendEmail(user, subject, content, overwriteVerify=False, pack=True, linkto=None, blockFurtherMessages=False): + Thread(target=sendEmailSend, args = (user, subject, content, overwriteVerify, pack, linkto, blockFurtherMessages)).start() -def sendEmailSend(user, subject, content, overwriteVerify): +def sendEmailSend(user, subject, content, overwriteVerify, pack, linkto, blockFurtherMessages): if (user.emailAddress["verified"] or overwriteVerify) and user.emailAddress["address"]: try: setupData = reutil.getSetupData(user.database) @@ -19,7 +19,13 @@ def sendEmailSend(user, subject, content, overwriteVerify): email_message.add_header('To', user.emailAddress["address"]) email_message.add_header('From', f"{setupData['name']} <{setupData['emailAddress']}>") email_message.add_header('Subject', translations.translate(subject, user.lang)) - html = f"<html><body>{translations.translate(content, user.lang)}{r.p(setupData['name']) if setupData['name'] else ''}</body></html>" + if pack: + content = r.p("{translation:mail/hello;name=%s}" % user.username) + content + (r.p(setupData['name']) if setupData['name'] else '') + if linkto: + content += r.p("{translation:mail/directLink}: "+r.link(linkto, linkto=linkto)) + if blockFurtherMessages: + content += r.p("{translation:mail/blockFurtherMessages}") + html = f"<html><body>{translations.translate(content, user.lang)}</body></html>" email_message.attach(MIMEText(util.htmlToText(html.replace("</p>", "</p>\n")), 'plain')) email_message.attach(MIMEText(html, 'html')) diff --git a/code/apps/mail/translations/de.json b/code/apps/mail/translations/de.json index d79e192..7c2223c 100644 --- a/code/apps/mail/translations/de.json +++ b/code/apps/mail/translations/de.json @@ -26,7 +26,8 @@ "emailVerified":"Deine Email Adresse wurde verifiziert." }, "hello":"Hallo {name},", - "changeNotifications":"Benachrichtigungen verwalten", + "directLink":"Direkter Link", + "blockFurtherMessages":"Diese Nachricht wird nicht für jede einzelne Änderung verschickt, sondern nur für die erste in einem gewissen Zeitraum.", "setup":{ "host":"Host", "port":"Port", diff --git a/code/apps/mail/translations/en.json b/code/apps/mail/translations/en.json index 8e42bb7..2a16198 100644 --- a/code/apps/mail/translations/en.json +++ b/code/apps/mail/translations/en.json @@ -26,7 +26,8 @@ "emailVerified":"Your email address is not verified." }, "hello":"Hello {name},", - "changeNotifications":"Manage notifications", + "directLink":"Direct link", + "blockFurtherMessages":"This message is not sent for every single change, but only for the first one in a certain period of time.", "setup":{ "host":"Host", "port":"Port", diff --git a/code/apps/notifications/code/mainapp.py b/code/apps/notifications/code/mainapp.py index 2630ec0..b134ae9 100644 --- a/code/apps/notifications/code/mainapp.py +++ b/code/apps/notifications/code/mainapp.py @@ -1,8 +1,43 @@ +import util + +from flask import request + +reutil = util.import_app_module("notifications", "reutil") +render = util.import_app_module("notifications", "render") + def notifications(user, data, path, modules): - return({"type":"packed", "content":"Not Yet"}) + loadscripts = ["root/ajax.js", "notifications/default.js"] + css = ["notifications/default.css"] + scripts = [] + content = "" + + events = reutil.getEventsByUser(user) + + if request.method == "GET": + if path == []: + content += render.renderAppOverview(events) + elif len(path) == 1 and path[0] in events: + content += render.renderEventsApp(user, path[0], events[path[0]]) + + return({"type":"packed", "content":content, "loadscripts":loadscripts, "css":css, "scripts":scripts}) + + elif request.method == "POST": + if "setNotifyEvent" in path: + appid, groupid, setting, value, eventid = util.ajaxdata(4) + groupid = groupid if groupid != "null" else None + newvalue = value == "false" + eventsToScan = events[appid]["events"] + if groupid: + for group in events[appid]["groups"]: + if group.id == groupid: + eventsToScan = group.events + for event in eventsToScan: + if event.id == eventid and setting in ["push", "activityLog", "email"]: + settings = event.getSettings() + settings[setting] = newvalue + event.saveSettings(settings) + return({"type":"unpacked","content":render.renderCheckbox(settings, event, setting)}) -def triggerNotification(user, eventname, ntcenter=None, email=None): - pass redirects={"/notifications":{"directto":notifications, "type":"nosession", "require_login":True}} diff --git a/code/apps/notifications/code/render.py b/code/apps/notifications/code/render.py new file mode 100644 index 0000000..6d58d02 --- /dev/null +++ b/code/apps/notifications/code/render.py @@ -0,0 +1,44 @@ +import sessionhelper as r +import util + +def renderAppOverview(events): + result = "" + for appid in events: + result += r.link("{translation:%s/title}" % appid, linkto="/notifications/"+appid, isbutton=True, classes=["widebutton"]) + return(r.createfieldset(legend=r.heading(3, "{translation:notifications/changeNotifications}"), content=result)) + +def renderEventsApp(user, appid, appEvents): + result = r.link("{translation:notifications/appEvents/backtoOverview}", linkto="/notifications", isbutton=True) + + result += r.heading(3, "{translation:%s/title}-{translation:notifications/title}" % (appid)) + + rows = [] + for item in appEvents["events"]: + rows.append(renderEventTableRow(item)) + + for group in appEvents["groups"]: + rows.append([{"str":group.getName(), "classes":["groupHeading"], "colspan":4}]) + for item in group.events: + rows.append(renderEventTableRow(item)) + + headers = ["{translation:notifications/appEvents/eventname}",r.center("{translation:notifications/appEvents/push}"), r.center("{translation:notifications/appEvents/activityLog}")] + + if "mail" in user.database.enabledApps(): + headers.append(r.center("{translation:notifications/appEvents/email}")) + + result += r.createtable(headers = headers, rows = rows, classes=["wide"]) + return(result) + +def renderEventTableRow(event): + settings = event.getSettings() + row = [ + event.getName(), + renderCheckbox(settings, event, "push"), + renderCheckbox(settings, event, "activityLog") + ] + if "mail" in event.data.enabledApps(): + row.append(renderCheckbox(settings, event, "email")) + return(row) + +def renderCheckbox(settings, event, setting): + return(r.center(r.createcheckboxButton(checked = settings[setting], otherattribues=event.checkBoxAttributes+["data-setting = '%s'" % setting, "data-groupid = '%s'" % (event.group.id if hasattr(event.group, "id") else "null")], onchange="return setNotifyEvent(this)", id=util.generate_token()))) diff --git a/code/apps/notifications/code/reutil.py b/code/apps/notifications/code/reutil.py index 1d94605..e0752c9 100644 --- a/code/apps/notifications/code/reutil.py +++ b/code/apps/notifications/code/reutil.py @@ -1,17 +1,100 @@ -class notificationEvent: - def __init__(self, user, appid, eventname): +import util, time + +class notification: + def getName(self): + return (self.name if self.name else "{translation:%s/notificationEvents/%s}" % (self.app.id, self.id)) + + def getSettings(self): + try: + return(util.read(self.settingsfile, importjson = True)) + except: + self.saveSettings({"activityLog":True, "push":True, "email":False}) + return(self.getSettings()) + + def saveSettings(self, settings): + util.write(self.settingsfile, settings) + + +class notificationEvent(notification): + def __init__(self, user, appid, eventid, eventname = None, settingsfile=None, timeBlocked=None): + self.data = user.database + self.user = user + self.app = self.data.apps[appid] + self.id = eventid + self.name = eventname + self.settingsfile = self.data.apps[appid].userdatapath(user=self.user)+"events/"+eventid+".json" if not settingsfile else settingsfile + self.group = None + self.checkBoxAttributes = ["data-appid = '%s'" % self.app.id, "data-eventid = '%s'" % str(self.id)] + self.timeBlocked = timeBlocked + +class notificationEventGroup(notification): + def __init__(self, user, appid, groupid, events, groupname = None, settingsfile=None): self.data = user.database self.user = user self.app = self.data.apps[appid] - self.eventname = eventname + self.id = groupid + self.name = groupname + self.settingsfile = self.data.apps[appid].userdatapath(user=self.user)+"eventsgroups/"+groupid+".json" if not settingsfile else settingsfile + self.events = events + for item in self.events: + item.group = self + def getAppEvents(user, appid): - if hasattr(user.database.apps[appid].code, getNotificicationEvents): - return(user.database.apps[appid].code.getNotificicationEvents(user)) - return([]) + if hasattr(user.database.apps[appid].code, "getNotificicationEvents"): + eventsAndGroups = user.database.apps[appid].code.getNotificicationEvents(user) + events, groups = [],[] + for item in eventsAndGroups: + (events if hasattr(item, "group") else groups).append(item) + return({"groups":groups, "events":events}) + return({"groups":[], "events":[]}) def getEventsByUser(user): result = {} for appid in user.database.enabledApps(): result[appid] = getAppEvents(user, appid) + if result[appid] == {"groups":[], "events":[]}: + result.pop(appid) return(result) + + +def triggerEvent(user, appid, eventid, ntcenter=None, email=None, activityLog=None, linkto=None): + if user.appEnabled[appid]: + events = getEventsByUser(user)[appid] + event = None + for item in events["events"]: + if item.id == eventid: + event = item + if not event: + for group in events["groups"]: + for item in group.events: + if item.id == eventid: + event = item + + if not (user.id in blockedEvents and eventid in blockedEvents[user.id] and blockedEvents[user.id][eventid] > time.time()): + if event.timeBlocked: + blockedEvents[user.id] = blockedEvents[user.id] if user.id in blockedEvents else {} + blockedEvents[user.id][eventid] = time.time() + event.timeBlocked + settings = event.getSettings() + if settings["push"] and ntcenter: + user.addUsermessage(ntcenter=ntcenter) + if settings["activityLog"] and activityLog: + user.addUsermessage(activityLog={"appid":appid, "eventname":event.getName(), "message":activityLog, "linkto":linkto}) + if settings["email"] and email: + email["linkto"] = linkto + email["blockFurtherMessages"] = event.timeBlocked != None + user.addUsermessage(email=email) + +def filterOutUser(user, allusers): + if type(allusers[0]) == str: + for index in range(len(allusers)): + allusers[index] = user.database.getUser(id=allusers[index]) + + result = [] + for userAll in allusers: + if userAll != user: + result.append(userAll) + return(result) + + +blockedEvents = {} diff --git a/code/apps/notifications/static/default.css b/code/apps/notifications/static/default.css new file mode 100644 index 0000000..544231c --- /dev/null +++ b/code/apps/notifications/static/default.css @@ -0,0 +1,9 @@ +fieldset{ + margin: 0.5em 0 0.5em 0 +} + +.groupHeading{ + font-weight: bold; + font-size: 125%; + padding-top: 0.7em +} diff --git a/code/apps/notifications/static/default.js b/code/apps/notifications/static/default.js new file mode 100644 index 0000000..4f3cfcb --- /dev/null +++ b/code/apps/notifications/static/default.js @@ -0,0 +1,20 @@ +function setNotifyEvent(button){ + js_ajax_page_updater(null, button.id, "notifications/setNotifyEvent", value=button.dataset.appid+"/"+button.dataset.groupid+"/"+button.dataset.setting+"/"+button.classList.contains("checked")+"/"+button.dataset.eventid) +} + +/*function setTableEventListener(){ + const tds = document.getElementsByTagName("td"); + for (i=0; i<tds.length;i++){ + tds[i].onclick = function(){ + const buttons = this.getElementsByClassName("checkbox") + if (buttons.length > 0){ + buttons[0].click() + } + } + }; +} + +window.addEventListener("load", setTableEventListener) + +=> If clicked on checkbox, function is called 2 times +*/ diff --git a/code/apps/notifications/translations/de.json b/code/apps/notifications/translations/de.json index 0967ef4..0311cab 100644 --- a/code/apps/notifications/translations/de.json +++ b/code/apps/notifications/translations/de.json @@ -1 +1,11 @@ -{} +{ + "title":"Benachrichtigungen", + "changeNotifications":"Benachrichtigungen verwalten", + "appEvents":{ + "backtoOverview":"Zurück zur Übersicht", + "eventname":"Eventname", + "push":"Push", + "activityLog":"Aktivitätsverlauf", + "email":"Email" + } +} diff --git a/code/apps/notifications/translations/en.json b/code/apps/notifications/translations/en.json new file mode 100644 index 0000000..9eb4cad --- /dev/null +++ b/code/apps/notifications/translations/en.json @@ -0,0 +1,11 @@ +{ + "title":"Notifications", + "changeNotifications":"Manage notifications", + "appEvents":{ + "backtoOverview":"Back to overview", + "eventname":"Eventname", + "push":"Push", + "activityLog":"Activity log", + "email":"Email" + } +} diff --git a/code/apps/profile/code/render.py b/code/apps/profile/code/render.py index aeef896..3422ab6 100644 --- a/code/apps/profile/code/render.py +++ b/code/apps/profile/code/render.py @@ -3,6 +3,10 @@ import sessionhelper as r def renderProfileSettings(user): result = renderLoginData(user) + result += r.createfieldset(legend=r.heading(3, "{translation:notifications/title}"), content= + r.center(r.link(visibleas="{translation:notifications/changeNotifications}", linkto="/notifications", isbutton=True)) + ) + if "mail" in user.database.enabledApps(): result += renderEmail(user) @@ -24,8 +28,6 @@ def renderEmail(user, changed = False): email += r.center(r.button(visibleas = "{translation:mail/newEmail/save}", onclick = "return changeEmail()", id="saveNewEmail")) else: email += r.center(r.p("{translation:mail/newEmail/verificationEmailSend}")) - - #email += r.center(r.link(visibleas="{translation:mail/changeNotifications}", linkto="/notifications", isbutton=True)) return(r.createfieldset(legend = r.heading(3, "{translation:mail/title}"), content = email, id = "emailFieldset")) def renderLoginData(user): diff --git a/code/apps/root/meta.json b/code/apps/root/meta.json index f20f216..3872d79 100644 --- a/code/apps/root/meta.json +++ b/code/apps/root/meta.json @@ -2,7 +2,7 @@ "version":"0.0.0", "id":"root", "showinheader":[ - ["{translation:root/header/login}", "/hello", -1] + ["{translation:root/header/login}", "/hello", -10] ], "showinfooter":[], "showapprights":false diff --git a/code/apps/root/static/default.css b/code/apps/root/static/default.css index f4eaa25..33c78b9 100644 --- a/code/apps/root/static/default.css +++ b/code/apps/root/static/default.css @@ -219,7 +219,7 @@ input, .button, select, textarea{ background-color: rgb(70, 70, 0); color: rgb(150,150,150) } -.widebutton, .centeredwindow{ +.widebutton, .centeredwindow, .wide{ width: -moz-available; width: -webkit-fill-available; } @@ -380,3 +380,29 @@ fieldset legend{ fieldset fieldset{ background-color: rgb(40,40,40) } + + +.checkbox, .backgroudImageButton{ + height: 1em; + min-width: 0em; + min-height: 0em; + width: 1em; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + display: inline-block; + color: rgba(0,0,0,0) +} + +.checkbox{ + border: 0.1em solid black; + border-radius: 0.4em; + background-color: rgb(200,200,200); + margin-right: 0.5em; + position: relative; + top: 0.25em; +} +.checkbox.checked{ + background-color: blue; + background-image: url("/static/startid/root/checkbox.png"); +} -- GitLab From ee84564fe3df7d592fcf42bcbd3daca079d4bd53 Mon Sep 17 00:00:00 2001 From: Lennart Franken <lennart@franken-familie.de> Date: Tue, 16 Aug 2022 21:26:02 +0200 Subject: [PATCH 2/2] notification fix 1 --- code/apps/lists/code/notification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/apps/lists/code/notification.py b/code/apps/lists/code/notification.py index 2242336..2c5dd8c 100644 --- a/code/apps/lists/code/notification.py +++ b/code/apps/lists/code/notification.py @@ -22,4 +22,4 @@ def triggerListEdited(userActing, list_obj): activityLog = text email = {"subject":"{translation:lists/notifications/listEdited/subject;listname=%s}" % list_obj.meta["name"], "content":text} for user in notify.filterOutUser(userActing, list_obj.getUsersWithAccess()): - notify.triggerEvent(user=user, appid="lists", eventid = "listChanged?"+list_obj.fullid, ntcenter=ntcenter, email=email, activityLog=activityLog) + notify.triggerEvent(user=user, appid="lists", eventid = "listChanged?"+list_obj.fullid, ntcenter=ntcenter, email=email, activityLog=activityLog, linkto=user.database.getDomain()+list_obj.linkto) -- GitLab