Index: html/file.item.html
===================================================================
--- html/file.item.html (revision 71557)
+++ html/file.item.html (working copy)
@@ -81,6 +81,16 @@
+
+
Index: html/msg.item.html
===================================================================
--- html/msg.item.html (revision 71557)
+++ html/msg.item.html (working copy)
@@ -124,6 +124,16 @@
+
+
Index: schema.py
===================================================================
--- schema.py (revision 71557)
+++ schema.py (working copy)
@@ -105,6 +105,8 @@
summary=String(),
files=Multilink("file"),
messageid=String(),
+ issueid=String(),
+ linked=Boolean(),
inreplyto=String(),
spambayes_score=Number(),
spambayes_misclassified=Boolean(),)
@@ -112,6 +114,8 @@
file = FileClass(db, "file",
name=String(),
description=String(indexme='yes'),
+ issueid=String(),
+ linked=Boolean(),
spambayes_score=Number(),
spambayes_misclassified=Boolean(),)
Index: detectors/unlinkauditor.py
===================================================================
--- detectors/unlinkauditor.py (revision 0)
+++ detectors/unlinkauditor.py (revision 0)
@@ -0,0 +1,68 @@
+# 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)
+ klass.set(gone, linked=False)
+ 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
+ no_issueid = not klass.get(added, 'issueid')
+ # Forbid re-linking already linked files and messages
+ linked = klass.get(added, 'linked')
+ add_remove_ok = add_remove_ok or (no_issueid and not linked)
+ if add_remove_ok:
+ if klass.get(added, 'issueid') == nodeid:
+ klass.set(added, issueid='')
+ klass.set(added, linked=True)
+ else:
+ raise ValueError(
+ "You don't have permission to link %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)