From 63c7919ccc9a6695f72c022f3380671cf67b979f Mon Sep 17 00:00:00 2001 From: Anon Ray Date: Thu, 30 May 2013 18:56:44 +0530 Subject: [PATCH] Add new menu config and Fix many bugs - Add a configuration system for writing customized navigation menus. The user has total flexibility to incorporate any kind of menus. Write any HTML and CSS you want. - Fix many bugs in adding new pages and rendering them. - Fix the backend API. Its more meaningful now. --- .gitignore | 1 + README.md | 88 ++++++++++++++++++++++++++--------------- mouchak.conf | 4 ++ readConfig.py | 22 +++++++++++ server.py | 105 +++++++++++++++++++++++++++++++++++++------------ static/css/editor.css | 7 +++- static/css/main.css | 1 - static/js/editor.js | 103 ++++++++++++++++++++++++++++++++++-------------- static/js/models.js | 23 ++++++++--- static/js/mouchak.js | 39 +++++++++++++++--- templates/editor.html | 56 ++++++++++++++++++++------ templates/index.html | 2 +- 12 files changed, 335 insertions(+), 116 deletions(-) create mode 100644 mouchak.conf create mode 100644 readConfig.py diff --git a/.gitignore b/.gitignore index e89c489..2538ed7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.*~ +*.pyc diff --git a/README.md b/README.md index cf041e6..d114337 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,66 @@ Mouchak ======= -A Javascript framework for building single page websites or web apps. +A Javascript framework for building websites quickly. -It takes the content of the website as a JSON. The JSON file can contain -text or multimedia content(images, audio, video). +It aims to provide a visual editing interface to create a website and edit its +content, primarily for non-technical users. -Mouchak can also load external JS/CSS files through the JSON. This gives the website to load -plugins to enhance/customize the website either through JS or CSS. +Under the hood, Mouchak abstracts the content of the website into a JSON +structure and uses Backbone model and views to render them dynamically. +This makes the site entirely run on the client side and syncs with the server +whenever there is a update. +This also makes Mouchak quite extensible. The built-in types to represent HTML +elements and media and such can be used and extended to build more custom +types. + +Mouchak can also load external JS/CSS files. Any magic that can be done using +Javascript and CSS can be easily integrated into Mouchak. Just specify Mouchak +the plugin to load and the callback to execute. + +Additionally, Mouchak can also provide a semantic rich environment. It provides +the user to specify tags or keywords with each associated content. To +complement it, Mouchak provides an API (filterTags) to get all the related +content together. One can easily built a view if this kind of functionality is +necessary. How to use it ============= +Installing +---------- Either download this codebase or git clone the repo. -Once you have downloaded or cloned the repo, load the index.html file in your browser. -This loads the example content from example.json. +**Pre-requisites** + +You need to have Python, MongoDB, Flask and PyMongo. +To install python and mongodb on your platform, please use search engines to +find instructions. As they are quite popular softwares, getting help online +should not be difficult. + +To install Flask and PyMongo - +> pip install flask pymongo + +Configuring +----------- +Configuration of Mouchak consists of configuring which database to use, +hostname, port no, title of the website etc. +Open up mouchak.conf, edit the file according to your needs, and then save it. +Ideally, you should set up seperate db for every project. + +Running +------- +Once you have installed all the dependencies, go to the directory where the +code is located and type: +> python server.py + +This starts the Mouchak server. You can now point your browser to +[http://localhost:5000](http://localhost:5000) + +This will load up and display the website. +To edit the website go to [/edit](http://localhost:5000/edit) -Use the index.html file as the boilerplate file of your index.html file. -Modify the code in the script tag, which loads the example.json, and change -the URL to point to your JSON file. -Remember the JSON files is loaded in the client side. Hence your JSON file should -be from the same domain where this app is loaded. -See cross-domain policies for details. The global object for this framework is exposed as the variable M. This can be inspected in the console. @@ -39,7 +76,8 @@ It takes the content of the site in the form of JSON. The JSON should describe t (in terms of type like text, audio or image) and also provide the content (in case of text the content is as it is, in case images urls are given). The JSON should also describe the content semantically by giving it tags. -More details about the JSON format in example.json file. + +Mouchak provides a very simple and easy to use editor to edit the JSON. The framework provides an easy way to pull up related content by just specifying the tags. @@ -60,30 +98,18 @@ Mouchak uses HTML5 Boilerplate and Bootstrap project as a boilerplate code for t Mouchak also leverages powerful libraries like Backbone.js and Underscore.js to manage and render content. This gives more flexibility to the content of the website. -The main code resides in js/mouchak.js. The HTML markup it uses is in index.html. - -Javascript libary files are in js/lib. We use backbone.js, underscore.js and jquery in this -framework. - -Boilerplate code/files: -404.html - error template -crossdomain.xml - cross-domain policies to be obeyed by the client -css/bootstrap.css - boilerplate css -css/normalize.css - boilerplate css -css/main.css - boilerplate css -humans.txt - write your own credits -img/ - directory for images -robots.txt - crawl spider rules - Support ======= -Email to rayanon at janastu dot org / arvind at janastu dot org for any kind of feedback. +Email to rayanon at servelots dot com / arvind at servelots dot com for any kind of feedback. Issues ====== -Report issues [here](http://bugzilla.pantoto.org/bugzilla3/describecomponents.cgi?product=Mouchak) +Report issues [here](http://trac.pantoto.org/mouchak/) +First, check if your issue is already submitted by anyone else, by clicking on +"View Tickets". +If your issue is not reported, you can report it by clicking on "New Ticket". diff --git a/mouchak.conf b/mouchak.conf new file mode 100644 index 0000000..b5b16fd --- /dev/null +++ b/mouchak.conf @@ -0,0 +1,4 @@ +DB=test_mouchak +SITE_TITLE=Testing Mouchak +HOST=0.0.0.0 +PORT=5000 diff --git a/readConfig.py b/readConfig.py new file mode 100644 index 0000000..e0f5936 --- /dev/null +++ b/readConfig.py @@ -0,0 +1,22 @@ +#!/usr/bin/python + +# Read Mouchak Configuration + + +import re + +def readConfig(): + confFile = 'mouchak.conf' + fh = open(confFile, 'r') + contents = fh.read() + + match = re.search('DB=(.*)', contents) + dbName = match.group(1) + match = re.search('SITE_TITLE=(.*)', contents) + title = match.group(1) + match = re.search('HOST=(.*)', contents) + host = match.group(1) + match = re.search('PORT=(.*)', contents) + port = match.group(1) + + return {'db': dbName, 'site_title': title, 'host': host, 'port': int(port)} diff --git a/server.py b/server.py index 6721807..6e6f154 100644 --- a/server.py +++ b/server.py @@ -1,69 +1,122 @@ #!/usr/bin/python + # Mouchak Server - # A Flask Application (http://flask.pocoo.org/) import flask import pymongo import bson +import readConfig app = flask.Flask(__name__) + +config = readConfig.readConfig() + dbClient = pymongo.MongoClient() -db = dbClient['mouchak'] -collection = db['content'] +db = dbClient[config['db']] +siteContent = db['content'] +siteMenu = db['menu'] +if siteMenu.find_one() == None: + siteMenu.insert({'customMenu': False}) + # handy reference to otherwise long name bson.ObjId = bson.objectid.ObjectId + def getContent(): content = [] - for i in collection.find(): + for i in siteContent.find(): objId = bson.ObjId(i['_id']) del(i['_id']) i['id'] = str(objId) content.append(i) - return content + + menu = siteMenu.find_one() + objId = bson.ObjId(menu['_id']) + del(menu['_id']) + menu['id'] = str(objId) + + return {'content': content, 'menu': menu} + @app.route('/', methods=['GET']) def index(): - return flask.render_template('index.html', content=getContent()) + return flask.render_template('index.html', content=getContent(), + title=config['site_title']) -@app.route('/edit', methods=['GET', 'POST']) +@app.route('/edit', methods=['GET']) def edit(): - if flask.request.method == 'GET': - return flask.render_template('editor.html', content=getContent()) + return flask.render_template('editor.html', content=getContent(), + title=config['site_title']) - elif flask.request.method == 'POST': - newpage = flask.request.json - print newpage - res = collection.insert(newpage) - print res - return flask.jsonify(status='success')#, content=getContent()) - -@app.route('/edit/<_id>', methods=['PUT', 'DELETE']) -def editPage(_id): +@app.route('/page', methods=['POST']) +def insertPage(): + newpage = flask.request.json + print newpage + res = siteContent.insert(newpage) + _id = bson.ObjId(res) + newpage['id'] = str(_id) + del(newpage['_id']) + print newpage + # FIXME: handle errors + return flask.jsonify(status='ok', page=newpage) + + +@app.route('/page/<_id>', methods=['PUT', 'DELETE']) +def updatePage(_id): if flask.request.method == 'PUT': changedPage = flask.request.json print changedPage - res = collection.update({'_id' : bson.ObjId(_id)}, + print '=======' + res = siteContent.update({'_id': bson.ObjId(_id)}, changedPage) print res - #print collection.find({'name': changed['name']}) - #for i in collection.find({'name': changed['name']}): - #print i - return flask.jsonify(status='success')#, content=getContent()) + if res['err'] == None: + print changedPage + return flask.jsonify(status='ok', page=changedPage) elif flask.request.method == 'DELETE': delPage = flask.request.url print delPage print _id - res = collection.remove({'_id': bson.ObjId(_id)}) + res = siteContent.remove({'_id': bson.ObjId(_id)}) print res - return flask.jsonify(status='success', msg='removed') + if res['err'] == None: + return flask.jsonify(status='ok') + else: + return flask.jsonify(error=res['err'], status='error') -if __name__ == "__main__": - app.run(debug=True, host='0.0.0.0') +#@app.route('/menu', methods=['POST']) +#def insertMenu(): +# newmenu = flask.request.json +# print newmenu +# res = siteMenu.insert(newmenu) +# print res +# return flask.jsonify(status='success')#, content=getContent()) +# +@app.route('/menu/<_id>', methods=['PUT']) +def updateMenu(_id): + if flask.request.method == 'PUT': + changedMenu = flask.request.json + print changedMenu + res = siteMenu.update({'_id': bson.ObjId(_id)}, changedMenu) + print res + return flask.jsonify(status='ok',menu=changedMenu) + + #elif flask.request.method == 'DELETE': + # delMenu = flask.request.url + # print delMenu + # print _id + # res = siteMenu.remove({'_id': bson.ObjId(_id)}) + # return flask.jsonify(status='deleted') + + +if __name__ == "__main__": + print config + app.run(debug=True, host=config['host'], port=config['port']) diff --git a/static/css/editor.css b/static/css/editor.css index 72f967e..6f437a5 100644 --- a/static/css/editor.css +++ b/static/css/editor.css @@ -97,7 +97,7 @@ textarea { border: 1px solid #999; padding: 20px; width: 400px; - height: 400px; + /*height: 400px;*/ } #pages { position: absolute; @@ -106,7 +106,7 @@ textarea { border: 1px solid black; padding: 10px; width: 300px; - height: 80%; + /*height: 80%;*/ } #page { position: absolute; @@ -138,6 +138,9 @@ textarea { padding: 3px; margin-bottom: 10px; } +#addPage { + margin: 10px 0 0 200px; +} /* ========================================================================== Helper classes diff --git a/static/css/main.css b/static/css/main.css index ddfb7c0..de5b246 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -94,7 +94,6 @@ textarea { ========================================================================== */ - /* ========================================================================== Helper classes ========================================================================== */ diff --git a/static/js/editor.js b/static/js/editor.js index e84af3e..8e0860b 100644 --- a/static/js/editor.js +++ b/static/js/editor.js @@ -8,7 +8,8 @@ events: { 'click .pagename .disp': 'showPage', 'click #addPage': 'addPage', - 'click .pagename .remove': 'removePage' + 'click .pagename .remove': 'removePage', + 'click #menu-config': 'showMenu' }, initialize: function() { _.bindAll(this); @@ -19,6 +20,10 @@ $('#content-container').append(this.$el); this.$el.append(this.template()); this.$pagelist = $('#pagelist'); + var menu = M.site_content.menu; + //console.log(menu); + this.menuconfig = new M.types.model.menu(menu); + this.menuconfigview = new MenuConfigView({model: this.menuconfig}); }, render: function() { // append the page list @@ -37,7 +42,7 @@ M.editor.pageview = pageview; }, addPage: function() { - var newpage = new Page(); + var newpage = new M.types.model.Page(); M.pages.add(newpage); var newpageview = new PageView({model: newpage}); newpageview.render(); @@ -59,24 +64,15 @@ console.log('failed', model, xhr); } }); - } - }); - - var Page = Backbone.Model.extend({ - defaults: { - name: '', - title: '', - children: [], - content: [] }, - initialize: function() { - this.id = this.get('id'); + showMenu: function(event) { + this.menuconfigview.render(); } }); var Pages = Backbone.Collection.extend({ - model: Page, - url: '/edit' + model: M.types.model.Page, + url: '/page' }); /* view to manage each page and their properties - change page properties, @@ -115,15 +111,15 @@ console.log('name changed', page); }, render: function() { - console.log(this.$el); $('#page').html(''); - console.log('content: ', this.model.get('content')); + //console.log('content: ', this.model.get('content')); this.$el.html(this.template({ name: this.model.get('name'), title: this.model.get('title'), children: this.model.get('children'), - content: this.listContent() + content: this.listContent(), + checked: this.model.get('showNav') ? 'checked="checked"' : '' })); //hover effect @@ -132,7 +128,6 @@ }, function(event) { $(event.target).closest('.content-item').removeClass('alert-error') }); - console.log('done'); }, listContent: function() { var content = ''; @@ -187,7 +182,7 @@ console.log('recvd remove event..about to process..'); var content = this.model.get('content'); var idx = $(event.target).parent().attr('for'); - idx = Number(idx); + idx = Number(idx); //is this a correct way of doing it? console.log('remove content: ', content[idx]); content.splice(idx, 1); this.model.set({'content': content}); @@ -202,9 +197,18 @@ children = (children === '') ? [] : children.split(','); this.model.set({'name': name, 'title': title, 'children': children}); + if($('#showNav').is(':checked')) { + this.model.set({'showNav': true}); + } + else { + this.model.set({'showNav': false}); + } + this.model.save({}, { success: function(model, response) { console.log('saved', model, response); + model.set(response.page); + model.id = response.page.id; }, error: function(model, xhr) { console.log('failed', model, xhr); @@ -321,36 +325,75 @@ /* view to configure custom navigation menu */ var MenuConfigView = Backbone.View.extend({ - el: '#menu-config', + tagName: 'div', + id: 'page', events: { + 'change #custom-menu': 'customMenuChange', + 'click #updateMenu': 'saveMenu' }, initialize: function() { _.bindAll(this); this.template = _.template($('#menu-config-template').html()); }, render: function() { - $('#content-container').append(this.template({ - menu: 'foo' + $('#page').remove(); + $('#content-container').append(this.$el); + console.log('rendering..', this.$el); + this.$el.html(this.template({ + pos: this.model.get('pos'), + menu: this.model.get('html') })); + this.$menuOptions = $('.menu-options'); + + if(this.model.get('customMenu') === true) { + $('#custom-menu').attr('checked', true); + this.$menuOptions.show(); + } + }, + showMenuOptions: function(bool) { + if(bool === true) { + this.$menuOptions.show(); + } + else { + this.$menuOptions.hide(); + } + }, + customMenuChange: function(event) { + this.$menuOptions = $('.menu-options'); + if($('#custom-menu').is(':checked')) { + this.model.set({'customMenu': true}); + } + else { + this.model.set({'customMenu': false}); + } + this.showMenuOptions(this.model.get('customMenu')); + }, + saveMenu: function() { + var menuHTML = $('#menu').val().trim(); + this.model.set({'html': menuHTML}); + console.log(this.model.toJSON()); + this.model.save({}, { + success: function(model, response) { + }, + error: function(xhr, response) { + } + }); } }); M.editor = { init: function() { - M.pages = new Pages(M.site_content); + M.pages = new Pages(M.site_content.content); var pagelistview = new PageListView(); pagelistview.render(); M.pages.on('add', function(page) { pagelistview.render(); }); M.pagelistview = pagelistview; - - //var menuconfig = new MenuConfigView(); - //menuconfig.render(); } }; - function escapeHtml(string) { + var escapeHtml = function(string) { var entityMap = { "&": "&", "<": "<", @@ -360,8 +403,8 @@ "/": '/' }; return String(string).replace(/[&<>"'\/]/g, function (s) { - return entityMap[s]; - }); + return entityMap[s]; + }); } })(M); diff --git a/static/js/models.js b/static/js/models.js index aa1084f..944cc07 100644 --- a/static/js/models.js +++ b/static/js/models.js @@ -86,16 +86,14 @@ // model for each Page var Page = Backbone.Model.extend({ defaults: { - name: "index", + name: "", title: "", children: [], - content: [] + content: [], + showNav: true }, initialize: function() { - // adding the name of the model as its id. - // look up of this model through the collection - // is faster this way. - //this.set({id: M.sanitize(this.get('name'))}); + this.id = this.get('id'); this.set({id: this.get('id')}); } }); @@ -104,12 +102,25 @@ model: Page }); + var Menu = Backbone.Model.extend({ + defaults: { + customMenu: false + }, + url: function() { + return '/menu/' + this.id; + }, + initialize: function() { + this.id = this.get('id'); + }, + }); + //export types to the typemap M.types = M.types || {}; M.types.model = { 'text': Text, 'image': Image, 'video': Video, + 'menu': Menu, 'rss': RSS, 'table': Table, 'plugin': Plugin, diff --git a/static/js/mouchak.js b/static/js/mouchak.js index 3c3887a..108faee 100644 --- a/static/js/mouchak.js +++ b/static/js/mouchak.js @@ -13,7 +13,8 @@ var AppView = Backbone.View.extend({ _.bindAll(this); }, render: function() { - var navview = new NavigationView(); + var menu = new M.types.model.menu(M.site_content.menu); + var navview = new NavigationView({model: menu}); navview.render(); }, updateBreadcrumbs: function(event) { @@ -32,14 +33,24 @@ var NavigationView = Backbone.View.extend({ this.template = _.template($('#nav-bar-template').html()); }, render: function() { - this.$el.append(this.template({})); - this.$ul = $('.nav'); - this.populate(); + // if custom menu is not defined, render a default menu + console.log(this.model.toJSON()); + if(this.model.get('customMenu') === false) { + console.log('generating default menu..'); + this.$el.append(this.template({})); + this.$ul = $('.nav'); + this.populate(); + } + // else render the custom menu + else { + console.log('rendering custom menu..'); + this.$el.append(this.model.get('html')); + } }, populate: function() { var item_template = _.template($('#nav-item-template').html()); _.each(M.pages.models, function(page) { - console.log(_.isEmpty(page.get('children'))); + //console.log('no children?', _.isEmpty(page.get('children'))); this.$ul.append(item_template({ cls: (_.isEmpty(page.get('children'))) ? '' : 'dropdown', page: page.get('name') @@ -74,8 +85,24 @@ var AppRouter = Backbone.Router.extend({ M.rss_view.render(); } var id = nameIdMap[page]; + if(!id) { + this.render404(); + return; + } $('#'+id).show(); $('.'+page).show(); + if(M.pages.get(id).get('showNav') === false) { + $('#navigation').hide(); + } + else { + $('#navigation').show(); + } + }, + render404: function() { + $('.pageview').hide(); + var notFound = "Sorry, a page corresponding to your URL was not found.\n" + + "Maybe you have typed the URL wrong, or this page is no longer available."; + alert(notFound); } }); @@ -88,7 +115,7 @@ M.init = function() { M.tags = {}; //global tag cache // global collection of pages - M.pages = new types.model.Pages(M.site_content); + M.pages = new types.model.Pages(M.site_content.content); // iterate through pages to get their content and render them using views and // models diff --git a/templates/editor.html b/templates/editor.html index c2e5155..ec6e4cc 100644 --- a/templates/editor.html +++ b/templates/editor.html @@ -6,7 +6,7 @@ - Mouchak - ummm! + Editing.. | {{title}} @@ -24,14 +24,11 @@
-
-
- @@ -65,18 +83,22 @@

List of Pages

- -

Go to site

+ +
+

Site Menu

+

Go to site

+ @@ -104,6 +126,12 @@ value="<%= children %>"> +
+
+ Show Navigation + > +
+
<%= content %> @@ -111,6 +139,7 @@
+
@@ -124,8 +153,9 @@ - + + diff --git a/templates/index.html b/templates/index.html index f028086..ed3695f 100644 --- a/templates/index.html +++ b/templates/index.html @@ -6,7 +6,7 @@ - Mouchak - ummm! + {{title}} -- 1.7.10.4