# HG changeset patch # User Ludovic Chabant # Date 1355208052 28800 # Node ID c946f4facfa2f99798ef236346144b54de207b4b Initial commit. diff -r 000000000000 -r c946f4facfa2 .hgignore --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Mon Dec 10 22:40:52 2012 -0800 @@ -0,0 +1,5 @@ +syntax:glob +venv +*.pyc +*.pyo + diff -r 000000000000 -r c946f4facfa2 manage.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/manage.py Mon Dec 10 22:40:52 2012 -0800 @@ -0,0 +1,15 @@ +import os.path +from flask.ext.script import Manager, Command +from wikked import app + +manager = Manager(app) + +@manager.command +def stats(): + """Prints some stats about the wiki.""" + pass + + +if __name__ == "__main__": + manager.run() + diff -r 000000000000 -r c946f4facfa2 requirements.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/requirements.txt Mon Dec 10 22:40:52 2012 -0800 @@ -0,0 +1,10 @@ +Flask==0.9 +Flask-Login==0.1.3 +Flask-Script==0.5.1 +Flask-WTF==0.8 +Jinja2==2.6 +Markdown==2.2.1 +WTForms==1.0.2 +Werkzeug==0.8.3 +argparse==1.2.1 +wsgiref==0.1.2 diff -r 000000000000 -r c946f4facfa2 wikked/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/__init__.py Mon Dec 10 22:40:52 2012 -0800 @@ -0,0 +1,27 @@ +from flask import Flask + +# Create the main app. +app = Flask(__name__) +app.config.from_object('wikked.settings') +app.config.from_envvar('WIKKED_SETTINGS', silent=True) + +if app.config['DEBUG']: + from werkzeug import SharedDataMiddleware + import os + app.wsgi_app = SharedDataMiddleware(app.wsgi_app, { + '/': os.path.join(os.path.dirname(__file__), 'static') + }) + +# Login extension. +from flask.ext.login import LoginManager +login_manager = LoginManager() +login_manager.setup_app(app) + +# The main Wiki instance. +from wiki import Wiki +wiki = Wiki(logger=app.logger) + +# Import views and user loader. +import wikked.views +import wikked.auth + diff -r 000000000000 -r c946f4facfa2 wikked/auth.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/auth.py Mon Dec 10 22:40:52 2012 -0800 @@ -0,0 +1,27 @@ +from wikked import login_manager + +class User(object): + + username = '' + password = '' + email = '' + + def is_authenticated(self): + return True + + def is_active(self): + return True + + def is_anonymous(self): + return False + + def get_id(self): + return str(self.username) + + +@login_manager.user_loader +def load_user(userid): + try: + return User.objects.get(username=userid) + except: + return None diff -r 000000000000 -r c946f4facfa2 wikked/cache.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/cache.py Mon Dec 10 22:40:52 2012 -0800 @@ -0,0 +1,51 @@ +import os +import os.path + +try: + import simplejson as json +except ImportError: + import json + + +class Cache(object): + def __init__(self, root): + self.cache_dir = root + + def read(self, url, time): + path, valid = self._getCachePathAndValidity(url, time) + if valid: + with open(path, 'r') as f: + return json.load(f) + return None + + def write(self, url, data): + path = self._getCachePath(url) + if not os.path.isdir(os.path.dirname(path)): + os.makedirs(os.path.dirname(path)) + with open(path, 'w') as f: + json.dump(data, f) + + def remove(self, url): + path = self._getCachePath(url) + if os.path.isfile(path): + os.remove(path) + + def getTime(self, url): + path = self._getCachePath(url) + if not os.path.isfile(path): + return None + return os.path.getmtime(path) + + def _getCachePath(self, url): + return os.path.join(self.cache_dir, url) + + def _getCachePathAndValidity(self, url, time): + cache_path = self._getCachePath(url) + if not os.path.isfile(cache_path): + return cache_path, False + + if time >= os.path.getmtime(cache_path): + return cache_path, False + + return cache_path, True + diff -r 000000000000 -r c946f4facfa2 wikked/forms.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/forms.py Mon Dec 10 22:40:52 2012 -0800 @@ -0,0 +1,19 @@ +from wtforms import Form, BooleanField, TextField, TextAreaField, PasswordField, validators + + +class RegistrationForm(Form): + username = TextField('Username', [validators.Length(min=4, max=25)]) + email = TextField('Email Address', [validators.Length(min=6, max=35)]) + password = PasswordField('New Password', [ + validators.Required(), + validators.EqualTo('confirm', message='Passwords must match') + ]) + confirm = PasswordField('Repeat Password') + accept_tos = BooleanField('I accept the TOS', [validators.Required()]) + + +class EditPageForm(Form): + text = TextAreaField() + author = TextField('Author') + message = TextField('Message') + diff -r 000000000000 -r c946f4facfa2 wikked/fs.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/fs.py Mon Dec 10 22:40:52 2012 -0800 @@ -0,0 +1,83 @@ +import os +import os.path +import re +import string + + +class PageNotFoundError(Exception): + """ An error raised when no physical file + is found for a given URL. + """ + pass + + +class FileSystem(object): + """ A class responsible for mapping page URLs to + file-system paths, and for scanning the file-system + to list existing pages. + """ + def __init__(self, root): + self.root = root + self.excluded = [] + + def getPageNames(self, subdir=None): + basepath = self.root + if subdir is not None: + basepath = self.getPhysicalNamespacePath(subdir) + + for dirpath, dirnames, filenames in os.walk(basepath): + dirnames[:] = [d for d in dirnames if os.path.join(dirpath, d) not in self.excluded] + for filename in filenames: + path = os.path.join(dirpath, filename) + path_split = os.path.splitext(os.path.relpath(path, self.root)) + if path_split[1] != '': + yield path_split[0] + + def getPage(self, url): + path = self.getPhysicalPagePath(url) + with open(path, 'r') as f: + content = f.read() + name = os.path.basename(path) + name_split = os.path.splitext(name) + return { + 'url': url, + 'path': path, + 'name': name_split[0], + 'ext': name_split[1], + 'content': content + } + + def getPhysicalNamespacePath(self, url): + return self._getPhysicalPath(url, False) + + def getPhysicalPagePath(self, url): + return self._getPhysicalPath(url, True) + + def _getPhysicalPath(self, url, is_file): + if string.find(url, '..') >= 0: + raise ValueError("Page URLs can't contain '..': " + url) + + # For each "part" in the given URL, find the first + # file-system entry that would get slugified to an + # equal string. + current = self.root + parts = url.lower().split('/') + for i, part in enumerate(parts): + names = os.listdir(current) + for name in names: + name_formatted = re.sub(r'[^A-Za-z0-9_\.\-\(\)]+', '-', name.lower()) + if is_file and i == len(parts) - 1: + # If we're looking for a file and this is the last part, + # look for something similar but with an extension. + if re.match("%s\.[a-z]+" % re.escape(part), name_formatted): + current = os.path.join(current, name) + break + else: + if name_formatted == part: + current = os.path.join(current, name) + break + else: + # Failed to find a part of the URL. + raise PageNotFoundError("No such page: " + url) + return current + diff -r 000000000000 -r c946f4facfa2 wikked/models.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/models.py Mon Dec 10 22:40:52 2012 -0800 @@ -0,0 +1,4 @@ + +class Page(object): + pass + diff -r 000000000000 -r c946f4facfa2 wikked/scm.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/scm.py Mon Dec 10 22:40:52 2012 -0800 @@ -0,0 +1,140 @@ +import re +import os +import os.path +import logging +import tempfile +import subprocess + + +STATE_COMMITTED = 0 +STATE_MODIFIED = 1 +STATE_NEW = 2 + +class SourceControl(object): + def __init__(self, root, logger=None): + self.root = root + self.logger = logger + if logger is None: + self.logger = logging.getLogger('wikked.scm') + + def getSpecialDirs(self): + raise NotImplementedError() + + def getHistory(self, path): + raise NotImplementedError() + + def getState(self, path): + raise NotImplementedError() + + def commit(self, paths, op_meta): + raise NotImplementedError() + + def revert(self, paths=None): + raise NotImplementedError() + + +class PageRevision(object): + def __init__(self, rev_id=-1): + self.rev_id = rev_id + self.author = None + self.timestamp = 0 + self.description = None + + @property + def is_local(self): + return self.rev_id == -1 + + @property + def is_committed(self): + return self.rev_id != -1 + + +class MercurialSourceControl(SourceControl): + def __init__(self, root, logger=None): + SourceControl.__init__(self, root, logger) + self.hg = 'hg' + if not os.path.isdir(os.path.join(root, '.hg')): + self._run('init', root, norepo=True) + + ignore_path = os.path.join(root, '.hgignore') + if not os.path.isfile(ignore_path): + with open(ignore_path, 'w') as f: + f.write('.cache') + self._run('add', ignore_path) + self._run('commit', ignore_path, '-m', 'Created .hgignore.') + + def getSpecialDirs(self): + return [ os.path.join(self.root, '.hg') ] + + def getHistory(self, path): + st_out = self._run('status', path) + if len(st_out) > 0 and st_out[0] == '?': + return [ PageRevision() ] + + revisions = [] + log_out = self._run('log', path, '--template', '{rev} {node} [{author}] {date|localdate} {desc}\n') + for line in log_out.splitlines(): + m = re.match(r'(\d+) ([0-9a-f]+) \[([^\]]+)\] ([^ ]+) (.*)', line) + if m is None: + raise Exception('Error parsing history from Mercurial, got: ' + line) + rev = PageRevision() + rev.rev_id = int(m.group(1)) + rev.rev_hash = m.group(2) + rev.author = m.group(3) + rev.timestamp = float(m.group(4)) + rev.description = m.group(5) + revisions.append(rev) + return revisions + + def getState(self, path): + st_out = self._run('status', path) + if len(st_out) > 0: + if st_out[0] == '?' or st_out[0] == 'A': + return STATE_NEW + if st_out[0] == 'M': + return STATE_MODIFIED + return STATE_COMMITTED + + def commit(self, paths, op_meta): + if 'message' not in op_meta or not op_meta['message']: + raise ValueError("No commit message specified.") + + # Check if any of those paths needs to be added. + st_out = self._run('status', *paths) + add_paths = [] + for line in st_out.splitlines(): + if line[0] == '?': + add_paths.append(line[2:]) + if len(add_paths) > 0: + self._run('add', *paths) + + # Create a temp file with the commit message. + f, temp = tempfile.mkstemp() + with os.fdopen(f, 'w') as fd: + self.logger.debug("Saving message: " + op_meta['message']) + fd.write(op_meta['message']) + + # Commit and clean up the temp file. + try: + commit_args = list(paths) + [ '-l', temp ] + if 'author' in op_meta: + commit_args += [ '-u', op_meta['author'] ] + self._run('commit', *commit_args) + finally: + os.remove(temp) + + def revert(self, paths=None): + if paths is not None: + self._run('revert', '-C', paths) + else: + self._run('revert', '-a', '-C') + + def _run(self, cmd, *args, **kwargs): + exe = [ self.hg ] + if 'norepo' not in kwargs or not kwargs['norepo']: + exe += [ '-R', self.root ] + exe.append(cmd) + exe += args + self.logger.debug("Running Mercurial: " + str(exe)) + return subprocess.check_output(exe) + diff -r 000000000000 -r c946f4facfa2 wikked/settings.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/settings.py Mon Dec 10 22:40:52 2012 -0800 @@ -0,0 +1,4 @@ + +SECRET_KEY = '\xef)*\xbc\xd7\xa9t\x7f\xbc3pH1o\xc1\xe2\xb0\x19\\L\xeb\xe3\x00\xa3' +DEBUG = True + diff -r 000000000000 -r c946f4facfa2 wikked/static/js/backbone-min.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/static/js/backbone-min.js Mon Dec 10 22:40:52 2012 -0800 @@ -0,0 +1,38 @@ +// Backbone.js 0.9.2 + +// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. +// Backbone may be freely distributed under the MIT license. +// For all details and documentation: +// http://backbonejs.org +(function(){var l=this,y=l.Backbone,z=Array.prototype.slice,A=Array.prototype.splice,g;g="undefined"!==typeof exports?exports:l.Backbone={};g.VERSION="0.9.2";var f=l._;!f&&"undefined"!==typeof require&&(f=require("underscore"));var i=l.jQuery||l.Zepto||l.ender;g.setDomLibrary=function(a){i=a};g.noConflict=function(){l.Backbone=y;return this};g.emulateHTTP=!1;g.emulateJSON=!1;var p=/\s+/,k=g.Events={on:function(a,b,c){var d,e,f,g,j;if(!b)return this;a=a.split(p);for(d=this._callbacks||(this._callbacks= +{});e=a.shift();)f=(j=d[e])?j.tail:{},f.next=g={},f.context=c,f.callback=b,d[e]={tail:g,next:j?j.next:f};return this},off:function(a,b,c){var d,e,h,g,j,q;if(e=this._callbacks){if(!a&&!b&&!c)return delete this._callbacks,this;for(a=a?a.split(p):f.keys(e);d=a.shift();)if(h=e[d],delete e[d],h&&(b||c))for(g=h.tail;(h=h.next)!==g;)if(j=h.callback,q=h.context,b&&j!==b||c&&q!==c)this.on(d,j,q);return this}},trigger:function(a){var b,c,d,e,f,g;if(!(d=this._callbacks))return this;f=d.all;a=a.split(p);for(g= +z.call(arguments,1);b=a.shift();){if(c=d[b])for(e=c.tail;(c=c.next)!==e;)c.callback.apply(c.context||this,g);if(c=f){e=c.tail;for(b=[b].concat(g);(c=c.next)!==e;)c.callback.apply(c.context||this,b)}}return this}};k.bind=k.on;k.unbind=k.off;var o=g.Model=function(a,b){var c;a||(a={});b&&b.parse&&(a=this.parse(a));if(c=n(this,"defaults"))a=f.extend({},c,a);b&&b.collection&&(this.collection=b.collection);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.changed={};this._silent= +{};this._pending={};this.set(a,{silent:!0});this.changed={};this._silent={};this._pending={};this._previousAttributes=f.clone(this.attributes);this.initialize.apply(this,arguments)};f.extend(o.prototype,k,{changed:null,_silent:null,_pending:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.get(a);return this._escapedAttributes[a]=f.escape(null== +b?"":""+b)},has:function(a){return null!=this.get(a)},set:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c||(c={});if(!d)return this;d instanceof o&&(d=d.attributes);if(c.unset)for(e in d)d[e]=void 0;if(!this._validate(d,c))return!1;this.idAttribute in d&&(this.id=d[this.idAttribute]);var b=c.changes={},h=this.attributes,g=this._escapedAttributes,j=this._previousAttributes||{};for(e in d){a=d[e];if(!f.isEqual(h[e],a)||c.unset&&f.has(h,e))delete g[e],(c.silent?this._silent: +b)[e]=!0;c.unset?delete h[e]:h[e]=a;!f.isEqual(j[e],a)||f.has(h,e)!=f.has(j,e)?(this.changed[e]=a,c.silent||(this._pending[e]=!0)):(delete this.changed[e],delete this._pending[e])}c.silent||this.change(c);return this},unset:function(a,b){(b||(b={})).unset=!0;return this.set(a,null,b)},clear:function(a){(a||(a={})).unset=!0;return this.set(f.clone(this.attributes),a)},fetch:function(a){var a=a?f.clone(a):{},b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&&c(b,d)}; +a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},save:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c=c?f.clone(c):{};if(c.wait){if(!this._validate(d,c))return!1;e=f.clone(this.attributes)}a=f.extend({},c,{silent:!0});if(d&&!this.set(d,c.wait?a:c))return!1;var h=this,i=c.success;c.success=function(a,b,e){b=h.parse(a,e);if(c.wait){delete c.wait;b=f.extend(d||{},b)}if(!h.set(b,c))return false;i?i(h,a):h.trigger("sync",h,a,c)};c.error=g.wrapError(c.error, +h,c);b=this.isNew()?"create":"update";b=(this.sync||g.sync).call(this,b,this,c);c.wait&&this.set(e,a);return b},destroy:function(a){var a=a?f.clone(a):{},b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};if(this.isNew())return d(),!1;a.success=function(e){a.wait&&d();c?c(b,e):b.trigger("sync",b,e,a)};a.error=g.wrapError(a.error,b,a);var e=(this.sync||g.sync).call(this,"delete",this,a);a.wait||d();return e},url:function(){var a=n(this,"urlRoot")||n(this.collection,"url")||t(); +return this.isNew()?a:a+("/"==a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return null==this.id},change:function(a){a||(a={});var b=this._changing;this._changing=!0;for(var c in this._silent)this._pending[c]=!0;var d=f.extend({},a.changes,this._silent);this._silent={};for(c in d)this.trigger("change:"+c,this,this.get(c),a);if(b)return this;for(;!f.isEmpty(this._pending);){this._pending= +{};this.trigger("change",this,a);for(c in this.changed)!this._pending[c]&&!this._silent[c]&&delete this.changed[c];this._previousAttributes=f.clone(this.attributes)}this._changing=!1;return this},hasChanged:function(a){return!arguments.length?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):!1;var b,c=!1,d=this._previousAttributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return!arguments.length|| +!this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},isValid:function(){return!this.validate(this.attributes)},_validate:function(a,b){if(b.silent||!this.validate)return!0;var a=f.extend({},this.attributes,a),c=this.validate(a,b);if(!c)return!0;b&&b.error?b.error(this,c,b):this.trigger("error",this,c,b);return!1}});var r=g.Collection=function(a,b){b||(b={});b.model&&(this.model=b.model);b.comparator&&(this.comparator=b.comparator); +this._reset();this.initialize.apply(this,arguments);a&&this.reset(a,{silent:!0,parse:b.parse})};f.extend(r.prototype,k,{model:o,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},add:function(a,b){var c,d,e,g,i,j={},k={},l=[];b||(b={});a=f.isArray(a)?a.slice():[a];c=0;for(d=a.length;c=b))this.iframe=i('