Index: detectors/unlinkauditor.py =================================================================== --- detectors/unlinkauditor.py (revision 0) +++ detectors/unlinkauditor.py (revision 0) @@ -0,0 +1,63 @@ +# Python 2.3 ... 2.6 compatibility: +from roundup.anypy.sets_ import set + +# Roles that can always remove or restore files and messages +EDIT_ROLES = set('Developer Admin Coordinator'.split()) + +def removed(db, cl, nodeid, newvalues, name): + ''' Sets [msg|file].issueid on unlink, unsets on link + + Used for restoring removed files or messages in the web UI. + Also audits spurious removals and restorations by users. + ''' + if name == 'messages': + klass = db.msg + elif name == 'files': + klass = db.file + new = set(newvalues[name] or []) + old = set(cl.get(nodeid, name) or []) + len_new, len_old = len(new), len(old) + if len_new != len_old: + user = db.getuid() + roles = db.user.get(user, 'roles').split(',') + add_remove_ok = bool([role for role in roles if role in EDIT_ROLES]) + if len_new < len_old: + gone = (old - new).pop() + if name == 'files': + # Allow users to remove their own files + file_creator = klass.get(gone, 'creator') == user + add_remove_ok = add_remove_ok or file_creator + if add_remove_ok: + # Record issue id in removed file/msg + klass.set(gone, issueid=nodeid) + else: + raise ValueError( + "You don't have permission to remove %s %s." + % (name[:-1], gone)) + else: + added = (new - old).pop() + # Allow adding new files and messages that were + # not previously unlinked + add_remove_ok = add_remove_ok or not klass.get(added, 'issueid') + if add_remove_ok: + if klass.get(added, 'issueid') == nodeid: + klass.set(added, issueid='') + else: + raise ValueError( + "You don't have permission to remove %s %s." + % (name[:-1], added)) + +def removed_file(db, cl, nodeid, newvalues): + if 'files' not in newvalues or nodeid is None: + return + removed(db, cl, nodeid, newvalues, 'files') + +def removed_msg(db, cl, nodeid, newvalues): + if 'messages' not in newvalues or nodeid is None: + return + removed(db, cl, nodeid, newvalues, 'messages') + + +def init(db): + db.issue.audit('set', removed_msg) + db.issue.audit('set', removed_file) Index: share/roundup/templates/classic/html/file.item.html =================================================================== --- share/roundup/templates/classic/html/file.item.html (revision 4212) +++ share/roundup/templates/classic/html/file.item.html (working copy) @@ -46,6 +46,17 @@ tal:attributes="href string:file${context/id}/${context/name}" i18n:translate="">download +
+ + + +
+ + Index: share/roundup/templates/classic/html/msg.item.html =================================================================== --- share/roundup/templates/classic/html/msg.item.html (revision 4212) +++ share/roundup/templates/classic/html/msg.item.html (working copy) @@ -75,6 +75,16 @@ +
+ + + +
+ Index: share/roundup/templates/classic/schema.py =================================================================== --- share/roundup/templates/classic/schema.py (revision 4212) +++ share/roundup/templates/classic/schema.py (working copy) @@ -58,9 +58,11 @@ summary=String(), files=Multilink("file"), messageid=String(), + issueid=String(), inreplyto=String()) file = FileClass(db, "file", + issueid=String(), name=String()) # IssueClass automatically gets these properties in addition to the Class ones: