Index: trac/wiki/api.py =================================================================== --- trac/wiki/api.py (.../vendor/trac-0.9.4) (revision 25) +++ trac/wiki/api.py (.../branches/wikiflowpatch/0.9) (working copy) @@ -65,16 +65,35 @@ def get_link_resolvers(): """Return an iterable over (namespace, formatter) tuples.""" + +class IWikiWorkflowController(Interface): + + def get_default_version(name): + """Return default version integer for named page, or None if + plugin doesn't know.""" + def would_collide(req, name, version): + """Return true if saving this edit would cause a collision, + false if not, None if plugin doesn't know. Name is page name, + version is the version of text the edit was based on. + """ + +class IWikiModel(Interface): + + def get_page(name=None, version=None, db=None, latest=None): + """Get a WikiPage object.""" + class WikiSystem(Component): """Represents the wiki system.""" - implements(IWikiChangeListener, IWikiSyntaxProvider) + implements(IWikiChangeListener, IWikiSyntaxProvider, + IWikiWorkflowController) change_listeners = ExtensionPoint(IWikiChangeListener) macro_providers = ExtensionPoint(IWikiMacroProvider) syntax_providers = ExtensionPoint(IWikiSyntaxProvider) + workflow_controllers = ExtensionPoint(IWikiWorkflowController) INDEX_UPDATE_INTERVAL = 5 # seconds @@ -213,3 +232,11 @@ else: return '%s' \ % (formatter.href.wiki(page) + anchor, label) + + # IWikiWorkflowController default methods + + def get_default_version(self, name): + return None + + def would_collide(self, req, name, version): + return None Index: trac/wiki/web_ui.py =================================================================== --- trac/wiki/web_ui.py (.../vendor/trac-0.9.4) (revision 25) +++ trac/wiki/web_ui.py (.../branches/wikiflowpatch/0.9) (working copy) @@ -30,6 +30,7 @@ from trac.versioncontrol.diff import get_diff_options, hdf_diff from trac.web.chrome import add_link, add_stylesheet, INavigationContributor from trac.web import IRequestHandler +from trac.wiki.api import WikiSystem from trac.wiki.model import WikiPage from trac.wiki.formatter import wiki_to_html, wiki_to_oneliner @@ -63,7 +64,8 @@ # IRequestHandler methods def match_request(self, req): - match = re.match(r'^/wiki(?:/(.*))?', req.path_info) + # match = re.match(r'^/wiki(?:/(.*))?', req.path_info) + match = re.match(r'^/wiki(?:/(.*)|\W+)?$', req.path_info) if match: if match.group(1): req.args['page'] = match.group(1) @@ -79,12 +81,16 @@ add_stylesheet(req, 'common/css/wiki.css') + if action == 'edit': + default_page = WikiPage(self.env, pagename, None, db) + would_collide = page.would_collide(req) + if req.method == 'POST': if action == 'edit': - latest_version = WikiPage(self.env, pagename, None, db).version if req.args.has_key('cancel'): - req.redirect(self.env.href.wiki(page.name)) - elif int(version) != latest_version: + req.redirect(self.env.href.wiki(page.name, version=version)) + elif would_collide or (would_collide is None and + int(version) != default_page.version): action = 'collision' self._render_editor(req, db, page) elif req.args.has_key('preview'): @@ -101,11 +107,24 @@ elif action == 'delete': self._render_confirm(req, db, page) elif action == 'edit': + if version: + if would_collide is None: + # there is no workflow plugin, so we quietly + # revert to trac's old behavior of editing the + # default version of the page + page = default_page + elif would_collide: + action = 'collision' self._render_editor(req, db, page) elif action == 'diff': self._render_diff(req, db, page) elif action == 'history': - self._render_history(req, db, page) + # if workflow is in use, then the default version might + # not be the latest; to handle this case, we need to + # explicitly state that we want the entire history if req + # doesn't specify a version number + all_versions = not version + self._render_history(req, db, page, all_versions=all_versions) else: if req.args.get('format') == 'txt': req.send_response(200) @@ -155,13 +174,14 @@ req.perm.assert_permission('WIKI_DELETE') if req.args.has_key('cancel'): - req.redirect(self.env.href.wiki(page.name)) + req.redirect(self.env.href.wiki(page.name, version=page.version)) version = None if req.args.has_key('version'): version = int(req.args.get('version', 0)) page.delete(version, db) + # XXX why is there a commit here, but none after save() db.commit() if not page.exists: @@ -183,9 +203,12 @@ # WIKI_ADMIN page.readonly = int(req.args.has_key('readonly')) + # XXX definitely should pass db in here, so that change listeners + # can cause rollback in case they blow up; can't do that unless + # we later do a db.commit() somewhere though page.save(req.args.get('author'), req.args.get('comment'), req.remote_addr) - req.redirect(self.env.href.wiki(page.name)) + req.redirect(self.env.href.wiki(page.name, version=page.version)) def _render_confirm(self, req, db, page): if page.readonly: @@ -312,7 +335,7 @@ info['readonly'] = int(req.args.has_key('readonly')) req.hdf['wiki'] = info - def _render_history(self, req, db, page): + def _render_history(self, req, db, page, all_versions=0): """Extract the complete history for a given page and stores it in the HDF. @@ -327,7 +350,8 @@ req.hdf['title'] = page.name + ' (history)' history = [] - for version, t, author, comment, ipnr in page.get_history(): + for version, t, author, comment, ipnr in page.get_history( + all_versions=all_versions): history.append({ 'url': self.env.href.wiki(page.name, version=version), 'diff_url': self.env.href.wiki(page.name, Index: trac/wiki/model.py =================================================================== --- trac/wiki/model.py (.../vendor/trac-0.9.4) (revision 25) +++ trac/wiki/model.py (.../branches/wikiflowpatch/0.9) (working copy) @@ -20,17 +20,17 @@ import time from trac.core import * -from trac.wiki.api import WikiSystem +from trac.wiki.api import WikiSystem, IWikiModel - + class WikiPage(object): """Represents a wiki page (new or existing).""" - def __init__(self, env, name=None, version=None, db=None): + def __init__(self, env, name=None, version=None, db=None, latest=None): self.env = env self.name = name if name: - self._fetch(name, version, db) + self._fetch(name, version, db, latest) else: self.version = 0 self.text = '' @@ -38,10 +38,16 @@ self.old_text = self.text self.old_readonly = self.readonly - def _fetch(self, name, version=None, db=None): + def _fetch(self, name, version=None, db=None, latest=0): if not db: db = self.env.get_db_cnx() cursor = db.cursor() + if version is None and not latest: + for controller in WikiSystem(self.env).workflow_controllers: + version = controller.get_default_version(name) + if version is not None: + # the first controller that returns a non-None value wins + break if version: cursor.execute("SELECT version,text,readonly FROM wiki " "WHERE name=%s AND version=%s", @@ -61,6 +67,10 @@ self.text = '' self.readonly = 0 + def _next_version(self, db): + page = WikiPage(self.env, self.name, db=db, latest=1) + return page.version + 1 + exists = property(fget=lambda self: self.version > 0) def delete(self, version=None, db=None): @@ -110,13 +120,14 @@ if t is None: t = time.time() + next_version = self._next_version(db) if self.text != self.old_text: cursor = db.cursor() cursor.execute("INSERT INTO WIKI (name,version,time,author,ipnr," "text,comment,readonly) VALUES (%s,%s,%s,%s,%s,%s," - "%s,%s)", (self.name, self.version + 1, t, author, + "%s,%s)", (self.name, next_version, t, author, remote_addr, self.text, comment, self.readonly)) - self.version += 1 + self.version = next_version elif self.readonly != self.old_readonly: cursor = db.cursor() cursor.execute("UPDATE wiki SET readonly=%s WHERE name=%s", @@ -137,12 +148,53 @@ self.old_readonly = self.readonly self.old_text = self.text - def get_history(self, db=None): + def get_history(self, db=None, all_versions=0): if not db: db = self.env.get_db_cnx() cursor = db.cursor() - cursor.execute("SELECT version,time,author,comment,ipnr FROM wiki " - "WHERE name=%s AND version<=%s " - "ORDER BY version DESC", (self.name, self.version)) + if all_versions: + # show all page versions in history + cursor.execute("SELECT version,time,author,comment,ipnr FROM wiki " + "WHERE name=%s" + "ORDER BY version DESC", (self.name)) + else: + cursor.execute("SELECT version,time,author,comment,ipnr FROM wiki " + "WHERE name=%s AND version<=%s " + "ORDER BY version DESC", (self.name, self.version)) for version,time,author,comment,ipnr in cursor: yield version,time,author,comment,ipnr + + def would_collide(self, req): + """Return true if any workflow controllers detect collisions, + false if none do, and None if no controllers implement any + collision rules at all. + """ + would = None + for controller in WikiSystem(self.env).workflow_controllers: + w = controller.would_collide(req, self.name, self.version) + # This does the right thing for None, True, or False, with + # True taking precedence over False, and both taking + # precedence over None -- python is soooo nice. + would = would or w + return would + +class WikiModel(Component): + implements(IWikiModel) + + def get_page(self, name=None, version=None, db=None, latest=None): + """Get a WikiPage object.""" + return WikiPage(env=self.env, name=name, version=version, + db=db, latest=latest) + + def page_count(self, db=None): + if not db: + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT count(name) FROM wiki") + row = cursor.fetchone() + if row: + count = row[0] + else: + count = 0 + return count + Index: trac/web/api.py =================================================================== --- trac/web/api.py (.../vendor/trac-0.9.4) (revision 25) +++ trac/web/api.py (.../branches/wikiflowpatch/0.9) (working copy) @@ -240,7 +240,16 @@ simply send the response itself and not return anything. """ +class IRequestPostProcessor(Interface): + """Extension point interface for request post-processors. + Provides a clean way to override rendering behavior of existing + components.""" + def process(req, template, content_type): + """Do any post-processing the request might need; typically + adding values to req.hdf, or changing template or mime type. + Always returns template and content type, even if unchanged.""" + def absolute_url(req, path=None): """Reconstruct the absolute URL of the given request. Index: trac/web/main.py =================================================================== --- trac/web/main.py (.../vendor/trac-0.9.4) (revision 25) +++ trac/web/main.py (.../branches/wikiflowpatch/0.9) (working copy) @@ -23,7 +23,7 @@ from trac.perm import PermissionCache, PermissionError from trac.util import escape, enum, format_datetime, http_date, to_utf8, Markup from trac.web.api import absolute_url, Request, RequestDone, IAuthenticator, \ - IRequestHandler + IRequestHandler, IRequestPostProcessor from trac.web.chrome import Chrome from trac.web.clearsilver import HDFWrapper from trac.web.href import Href @@ -60,6 +60,7 @@ authenticators = ExtensionPoint(IAuthenticator) handlers = ExtensionPoint(IRequestHandler) + post_processors = ExtensionPoint(IRequestPostProcessor) def authenticate(self, req): for authenticator in self.authenticators: @@ -110,6 +111,10 @@ if not content_type: content_type = 'text/html' + for processor in self.post_processors: + template, content_type = processor.process( + req, template, content_type) + req.display(template, content_type or 'text/html') finally: # Give the session a chance to persist changes Index: templates/wiki.cs =================================================================== --- templates/wiki.cs (.../vendor/trac-0.9.4) (revision 25) +++ templates/wiki.cs (.../branches/wikiflowpatch/0.9) (working copy) @@ -289,6 +289,9 @@ if:trac.acl.WIKI_MODIFY ?>
+ + +