# HG changeset patch # User Ludovic Chabant # Date 1442469868 25200 # Node ID 37f426e067c45c1408e31d2ed46c6e98f89e7f59 # Parent ef12066349399148b2cf03f38b0c97bd02b11007 Big refactor to get rid of this whole single page app crap. Getting back to basics! diff -r ef1206634939 -r 37f426e067c4 Gruntfile.js --- a/Gruntfile.js Sun Aug 30 21:45:42 2015 -0700 +++ b/Gruntfile.js Wed Sep 16 23:04:28 2015 -0700 @@ -1,3 +1,7 @@ +/*global module:false*/ +'use strict'; + + module.exports = function(grunt) { // Project configuration. @@ -26,19 +30,37 @@ development: { options: { optimize: "none", - baseUrl: "wikked/assets", + baseUrl: "wikked/assets/js", mainConfigFile: "wikked/assets/js/wikked.js", - name: "js/wikked", - out: "wikked/static/js/wikked.min.js" + dir: "wikked/static/js", + modules: [ + { + name: "wikked.app", + include: ["require.js"] + }, + { + name: "wikked.edit", + exclude: ["wikked.app"] + } + ] } }, production: { options: { optimize: "uglify", - baseUrl: "wikked/assets", + baseUrl: "wikked/assets/js", mainConfigFile: "wikked/assets/js/wikked.js", - name: "js/wikked", - out: "wikked/static/js/wikked.min.js" + dir: "wikked/static/js", + modules: [ + { + name: "wikked.app", + include: ["require.js"] + }, + { + name: "wikked.edit", + exclude: ["wikked.app"] + } + ] } } }, @@ -56,9 +78,6 @@ development: { files: [ //{expand: true, cwd: 'wikked/assets/', dest: 'wikked/static/', src: ['img/**']}, - {expand: true, cwd: 'wikked/assets/', dest: 'wikked/static/', src: ['js/**']}, - {expand: true, cwd: 'wikked/assets/', dest: 'wikked/static/', src: ['tpl/**']}, - {expand: true, cwd: 'wikked/assets/', dest: 'wikked/static/', src: ['json/**']}, {expand: true, cwd: 'wikked/assets/font-awesome', dest: 'wikked/static/', src: ['fonts/**']} ] }, @@ -67,12 +86,6 @@ {expand: true, cwd: 'wikked/assets/', dest: 'wikked/static/', src: ['js/wikked.js', 'js/wikked/**']} ] }, - dev_templates: { - files: [ - {expand: true, cwd: 'wikked/assets/', dest: 'wikked/static/', src: ['tpl/**']}, - {expand: true, cwd: 'wikked/assets/', dest: 'wikked/static/', src: ['json/**']} - ] - }, production: { files: [ {expand: true, cwd: 'wikked/assets/', dest: 'wikked/static/', src: ['js/require.js']}, @@ -89,10 +102,6 @@ files: ['wikked/assets/js/wikked.js', 'wikked/assets/js/wikked/**'], tasks: ['jshint:all', 'copy:dev_scripts'] }, - templates: { - files: ['wikked/assets/tpl/**/*.html', 'wikked/assets/json/**/*.json'], - tasks: ['copy:dev_templates'] - }, styles: { files: ['wikked/assets/css/**/*.less'], tasks: ['less:development'] @@ -116,6 +125,6 @@ grunt.registerTask('default', ['jshint', 'less:production', 'requirejs:production', 'imagemin:all', 'copy:production']); // Other tasks. - grunt.registerTask('dev', ['less:development', 'copy:production', 'copy:development']); + grunt.registerTask('dev', ['less:development', 'requirejs:development', 'copy:production', 'copy:development']); }; diff -r ef1206634939 -r 37f426e067c4 wikked/api/__init__.py diff -r ef1206634939 -r 37f426e067c4 wikked/api/admin.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/api/admin.py Wed Sep 16 23:04:28 2015 -0700 @@ -0,0 +1,69 @@ +from flask import jsonify, abort, request +from flask.ext.login import logout_user, current_user +from wikked.web import app, get_wiki, login_manager +from wikked.webimpl.admin import do_login_user + + +@app.route('/api/admin/reindex', methods=['POST']) +def api_admin_reindex(): + if not current_user.is_authenticated() or not current_user.is_admin(): + return login_manager.unauthorized() + wiki = get_wiki() + wiki.index.reset(wiki.getPages()) + result = {'ok': 1} + return jsonify(result) + + +@app.route('/api/user/login', methods=['POST']) +def api_user_login(): + if do_login_user(): + username = request.form.get('username') + result = {'username': username, 'logged_in': 1} + return jsonify(result) + abort(401) + + +@app.route('/api/user/is_logged_in') +def api_user_is_logged_in(): + if current_user.is_authenticated(): + result = {'logged_in': True} + return jsonify(result) + abort(401) + + +@app.route('/api/user/logout', methods=['POST']) +def api_user_logout(): + logout_user() + result = {'ok': 1} + return jsonify(result) + + +@app.route('/api/user/info') +def api_current_user_info(): + user = current_user + if user.is_authenticated(): + result = { + 'user': { + 'username': user.username, + 'groups': user.groups + } + } + return jsonify(result) + return jsonify({'user': False}) + + +@app.route('/api/user/info/') +def api_user_info(name): + wiki = get_wiki() + user = wiki.auth.getUser(name) + if user is not None: + result = { + 'user': { + 'username': user.username, + 'groups': user.groups + } + } + return jsonify(result) + abort(404) + + diff -r ef1206634939 -r 37f426e067c4 wikked/api/edit.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/api/edit.py Wed Sep 16 23:04:28 2015 -0700 @@ -0,0 +1,85 @@ +from flask import abort, request, jsonify +from flask.ext.login import current_user +from wikked.web import app, get_wiki +from wikked.webimpl import url_from_viewarg, split_url_from_viewarg +from wikked.webimpl.edit import ( + get_edit_page, do_edit_page, preview_edited_page) + + +@app.route('/api/edit/', methods=['GET', 'POST']) +def api_edit_page(url): + wiki = get_wiki() + user = current_user.get_id() + endpoint, path = split_url_from_viewarg(url) + + if request.method == 'GET': + url = path + custom_data = None + if endpoint is not None: + url = '%s:%s' % (endpoint, path) + custom_data = { + 'meta_query': endpoint, + 'meta_value': path.lstrip('/') + } + + data = get_edit_page( + wiki, user, url, + custom_data=custom_data) + return jsonify(data) + + if request.method == 'POST': + url = path + if endpoint is not None: + url = '%s:%s' % (endpoint, path) + + author = user or request.form.get('author') + if not author: + abort(400) + + message = request.form.get('message') + if not message: + abort(400) + + do_edit_page(wiki, user, url, author, message) + return jsonify({'edited': True}) + + +@app.route('/api/preview', methods=['POST']) +def api_preview(): + url = request.form.get('url') + url = url_from_viewarg(url) + text = request.form.get('text') + wiki = get_wiki() + preview = preview_edited_page(wiki, url, text) + result = {'text': preview} + return jsonify(result) + + +@app.route('/api/rename/', methods=['POST']) +def api_rename_page(url): + pass + + +@app.route('/api/delete/', methods=['POST']) +def api_delete_page(url): + pass + + +@app.route('/api/validate/newpage', methods=['GET', 'POST']) +def api_validate_newpage(): + path = request.form.get('title') + if path is None: + abort(400) + + path = url_from_viewarg(path) + try: + # Check that there's no page with that name already, and that + # the name can be correctly mapped to a filename. + wiki = get_wiki() + if wiki.pageExists(path): + raise Exception("Page '%s' already exists" % path) + wiki.fs.getPhysicalPagePath(path, make_new=True) + except Exception: + return '"This page name is invalid or unavailable"' + return '"true"' + diff -r ef1206634939 -r 37f426e067c4 wikked/api/history.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/api/history.py Wed Sep 16 23:04:28 2015 -0700 @@ -0,0 +1,86 @@ +from flask import request, abort, jsonify +from flask.ext.login import current_user +from wikked.web import app, get_wiki +from wikked.webimpl import url_from_viewarg +from wikked.webimpl.history import ( + get_site_history, get_page_history, + read_page_rev, diff_page_revs) + + +@app.route('/api/site-history') +def api_site_history(): + wiki = get_wiki() + user = current_user.get_id() + after_rev = request.args.get('rev') + result = get_site_history(wiki, user, after_rev=after_rev) + return jsonify(result) + + +@app.route('/api/history/') +def api_main_page_history(): + wiki = get_wiki() + return api_page_history(wiki.main_page_url.lstrip('/')) + + +@app.route('/api/history/') +def api_page_history(url): + wiki = get_wiki() + user = current_user.get_id() + url = url_from_viewarg(url) + result = get_page_history(wiki, user, url) + return jsonify(result) + + +@app.route('/api/revision/') +def api_read_page_rev(url): + wiki = get_wiki() + user = current_user.get_id() + url = url_from_viewarg(url) + rev = request.args.get('rev') + if rev is None: + abort(400) + result = read_page_rev(wiki, user, url, rev=rev) + return jsonify(result) + + +@app.route('/api/diff/') +def api_diff_page(url): + wiki = get_wiki() + user = current_user.get_id() + url = url_from_viewarg(url) + rev1 = request.args.get('rev1') + rev2 = request.args.get('rev2') + raw = request.args.get('raw') + if rev1 is None: + abort(400) + result = diff_page_revs(wiki, user, url, + rev1=rev1, rev2=rev2, raw=raw) + return jsonify(result) + + +@app.route('/api/revert/', methods=['POST']) +def api_revert_page(url): + # TODO: only users with write access can revert. + if 'rev' not in request.form: + abort(400) + rev = request.form['rev'] + + author = request.remote_addr + if 'author' in request.form and len(request.form['author']) > 0: + author = request.form['author'] + + message = 'Reverted %s to revision %s' % (url, rev) + if 'message' in request.form and len(request.form['message']) > 0: + message = request.form['message'] + + url = url_from_viewarg(url) + page_fields = { + 'rev': rev, + 'author': author, + 'message': message + } + wiki = get_wiki() + wiki.revertPage(url, page_fields) + result = {'reverted': 1} + return jsonify(result) + diff -r ef1206634939 -r 37f426e067c4 wikked/api/read.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/api/read.py Wed Sep 16 23:04:28 2015 -0700 @@ -0,0 +1,135 @@ +from flask import request, jsonify, make_response, abort +from flask.ext.login import current_user +from wikked.scm.base import STATE_NAMES +from wikked.utils import PageNotFoundError +from wikked.web import app, get_wiki +from wikked.webimpl import ( + CHECK_FOR_READ, + url_from_viewarg, + get_page_or_raise, get_page_or_none, + get_page_meta, is_page_readable, + RedirectNotFound, CircularRedirectError) +from wikked.webimpl.read import ( + read_page, get_incoming_links, get_outgoing_links) +from wikked.webimpl.special import list_pages + + +@app.route('/api/list') +def api_list_all_pages(): + return api_list_pages(None) + + +@app.route('/api/list/') +def api_list_pages(url): + wiki = get_wiki() + url = url_from_viewarg(url) + result = list_pages(wiki, current_user.get_id(), url=url) + return jsonify(result) + + +@app.route('/api/read/') +def api_read_main_page(): + wiki = get_wiki() + return api_read_page(wiki.main_page_url.lstrip('/')) + + +@app.route('/api/read/') +def api_read_page(url): + wiki = get_wiki() + user = current_user.get_id() + try: + result = read_page(wiki, user, url) + return jsonify(result) + except RedirectNotFound as e: + app.logger.exception(e) + abort(404) + except CircularRedirectError as e: + app.logger.exception(e) + abort(409) + + +@app.route('/api/raw/') +def api_read_main_page_raw(): + wiki = get_wiki() + return api_read_page_raw(wiki.main_page_url.lstrip('/')) + + +@app.route('/api/raw/') +def api_read_page_raw(url): + wiki = get_wiki() + user = current_user.get_id() + url = url_from_viewarg(url) + try: + page = get_page_or_raise( + wiki, url, + check_perms=(user, CHECK_FOR_READ), + fields=['raw_text', 'meta']) + except PageNotFoundError as e: + app.logger.exception(e) + abort(404) + resp = make_response(page.raw_text) + resp.mimetype = 'text/plain' + return resp + + +@app.route('/api/query') +def api_query(): + wiki = get_wiki() + query = dict(request.args) + pages = wiki.getPages(meta_query=query) + result = { + 'query': query, + 'pages': [get_page_meta(p) for p in pages] + } + return jsonify(result) + + +@app.route('/api/state/') +def api_get_main_page_state(): + wiki = get_wiki() + return api_get_state(wiki.main_page_url.lstrip('/')) + + +@app.route('/api/state/') +def api_get_state(url): + wiki = get_wiki() + user = current_user.get_id() + page = get_page_or_raise( + wiki, url, + check_perms=(user, CHECK_FOR_READ), + fields=['url', 'title', 'path', 'meta']) + state = page.getState() + return jsonify({ + 'meta': get_page_meta(page, True), + 'state': STATE_NAMES[state] + }) + + +@app.route('/api/outlinks/') +def api_get_main_page_outgoing_links(): + wiki = get_wiki() + return api_get_outgoing_links(wiki.main_page_url.lstrip('/')) + + +@app.route('/api/outlinks/') +def api_get_outgoing_links(url): + wiki = get_wiki() + user = current_user.get_id() + result = get_outgoing_links(wiki, user, url) + return jsonify(result) + + +@app.route('/api/inlinks/') +def api_get_main_page_incoming_links(): + wiki = get_wiki() + return api_get_incoming_links(wiki.main_page_url.lstrip('/')) + + +@app.route('/api/inlinks/') +def api_get_incoming_links(url): + wiki = get_wiki() + user = current_user.get_id() + result = get_incoming_links(wiki, user, url) + return jsonify(result) + + diff -r ef1206634939 -r 37f426e067c4 wikked/api/special.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/api/special.py Wed Sep 16 23:04:28 2015 -0700 @@ -0,0 +1,51 @@ +from flask import jsonify, request, abort +from flask.ext.login import current_user +from wikked.web import app, get_wiki +from wikked.webimpl.special import ( + get_orphans, get_broken_redirects, get_double_redirects, + get_dead_ends, get_search_results, get_search_preview_results) + + +def call_api(api_func, *args, **kwargs): + wiki = get_wiki() + user = current_user.get_id() + result = api_func(wiki, user, *args, **kwargs) + return jsonify(result) + + +@app.route('/api/orphans') +def api_special_orphans(): + return call_api(get_orphans) + + +@app.route('/api/broken-redirects') +def api_special_broken_redirects(): + return call_api(get_broken_redirects) + + +@app.route('/api/double-redirects') +def api_special_double_redirects(): + return call_api(get_double_redirects) + + +@app.route('/api/dead-ends') +def api_special_dead_ends(): + return call_api(get_dead_ends) + + +@app.route('/api/search') +def api_search(): + query = request.args.get('q') + if query is None or query == '': + abort(400) + return call_api(get_search_results, query=query) + + +@app.route('/api/searchpreview') +def api_search_preview(): + query = request.args.get('q') + if query is None or query == '': + abort(400) + return call_api(get_search_preview_results, query=query) + + diff -r ef1206634939 -r 37f426e067c4 wikked/assets/css/wikked/nav.less --- a/wikked/assets/css/wikked/nav.less Sun Aug 30 21:45:42 2015 -0700 +++ b/wikked/assets/css/wikked/nav.less Wed Sep 16 23:04:28 2015 -0700 @@ -24,6 +24,13 @@ text-decoration: none; } +#wiki-menu-shortcut { + &:hover { + color: @color-gray-light; + text-shadow: #EEE 0 0 1em; + } +} + #wiki-menu-pin { height: @wiki-shortcut-height; padding: 0 1.5em 0 @wiki-shortcut-width; @@ -33,12 +40,16 @@ &:hover { color: @color-gray-light; + text-shadow: #EEE 0 0 1em; } span { line-height: @wiki-shortcut-height; } } +#wiki-menu-pin.wiki-menu-pin-active { + color: @color-gray-light; +} #wiki-menu { position: fixed !important; diff -r ef1206634939 -r 37f426e067c4 wikked/assets/js/backbone-min.js --- a/wikked/assets/js/backbone-min.js Sun Aug 30 21:45:42 2015 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,38 +0,0 @@ -// 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('