Commit 63c7919ccc9a6695f72c022f3380671cf67b979f

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 / 0)
  
11
22*.*~
3*.pyc
README.md
(57 / 31)
  
11Mouchak
22=======
33
4A Javascript framework for building single page websites or web apps.
4A Javascript framework for building websites quickly.
55
6It takes the content of the website as a JSON. The JSON file can contain
7text or multimedia content(images, audio, video).
6It aims to provide a visual editing interface to create a website and edit its
7content, primarily for non-technical users.
88
9Mouchak can also load external JS/CSS files through the JSON. This gives the website to load
10plugins to enhance/customize the website either through JS or CSS.
9Under the hood, Mouchak abstracts the content of the website into a JSON
10structure and uses Backbone model and views to render them dynamically.
11This makes the site entirely run on the client side and syncs with the server
12whenever there is a update.
13This also makes Mouchak quite extensible. The built-in types to represent HTML
14elements and media and such can be used and extended to build more custom
15types.
1116
17Mouchak can also load external JS/CSS files. Any magic that can be done using
18Javascript and CSS can be easily integrated into Mouchak. Just specify Mouchak
19the plugin to load and the callback to execute.
1220
21Additionally, Mouchak can also provide a semantic rich environment. It provides
22the user to specify tags or keywords with each associated content. To
23complement it, Mouchak provides an API (filterTags) to get all the related
24content together. One can easily built a view if this kind of functionality is
25necessary.
26
27
1328How to use it
1429=============
1530
31Installing
32----------
1633Either download this codebase or git clone the repo.
1734
18Once you have downloaded or cloned the repo, load the index.html file in your browser.
19This loads the example content from example.json.
35**Pre-requisites**
2036
21Use the index.html file as the boilerplate file of your index.html file.
22Modify the code in the script tag, which loads the example.json, and change
23the URL to point to your JSON file.
24Remember the JSON files is loaded in the client side. Hence your JSON file should
25be from the same domain where this app is loaded.
26See cross-domain policies for details.
37You need to have Python, MongoDB, Flask and PyMongo.
38To install python and mongodb on your platform, please use search engines to
39find instructions. As they are quite popular softwares, getting help online
40should not be difficult.
2741
42To install Flask and PyMongo -
43> pip install flask pymongo
44
45Configuring
46-----------
47Configuration of Mouchak consists of configuring which database to use,
48hostname, port no, title of the website etc.
49Open up mouchak.conf, edit the file according to your needs, and then save it.
50Ideally, you should set up seperate db for every project.
51
52Running
53-------
54Once you have installed all the dependencies, go to the directory where the
55code is located and type:
56> python server.py
57
58This starts the Mouchak server. You can now point your browser to
59[http://localhost:5000](http://localhost:5000)
60
61This will load up and display the website.
62To edit the website go to [/edit](http://localhost:5000/edit)
63
64
2865The global object for this framework is exposed as the variable M. This can be
2966inspected in the console.
3067
7676(in terms of type like text, audio or image) and also provide the content (in case of text
7777the content is as it is, in case images urls are given). The JSON should also describe the
7878content semantically by giving it tags.
79More details about the JSON format in example.json file.
8079
80Mouchak provides a very simple and easy to use editor to edit the JSON.
81
8182The framework provides an easy way to pull up related content by just specifying the tags.
8283
8384Backbone models are used to model the content. Different content has different types.
9898Mouchak also leverages powerful libraries like Backbone.js and Underscore.js to manage and render
9999content. This gives more flexibility to the content of the website.
100100
101The main code resides in js/mouchak.js. The HTML markup it uses is in index.html.
102101
103Javascript libary files are in js/lib. We use backbone.js, underscore.js and jquery in this
104framework.
105
106Boilerplate code/files:
107404.html - error template
108crossdomain.xml - cross-domain policies to be obeyed by the client
109css/bootstrap.css - boilerplate css
110css/normalize.css - boilerplate css
111css/main.css - boilerplate css
112humans.txt - write your own credits
113img/ - directory for images
114robots.txt - crawl spider rules
115
116
117102Support
118103=======
119104
120Email to rayanon at janastu dot org / arvind at janastu dot org for any kind of feedback.
105Email to rayanon at servelots dot com / arvind at servelots dot com for any kind of feedback.
121106
122107
123108Issues
124109======
125110
126Report issues [here](http://bugzilla.pantoto.org/bugzilla3/describecomponents.cgi?product=Mouchak)
111Report issues [here](http://trac.pantoto.org/mouchak/)
112First, check if your issue is already submitted by anyone else, by clicking on
113"View Tickets".
114If your issue is not reported, you can report it by clicking on "New Ticket".
  
1DB=test_mouchak
2SITE_TITLE=Testing Mouchak
3HOST=0.0.0.0
4PORT=5000
  
1#!/usr/bin/python
2
3# Read Mouchak Configuration
4
5
6import re
7
8def readConfig():
9 confFile = 'mouchak.conf'
10 fh = open(confFile, 'r')
11 contents = fh.read()
12
13 match = re.search('DB=(.*)', contents)
14 dbName = match.group(1)
15 match = re.search('SITE_TITLE=(.*)', contents)
16 title = match.group(1)
17 match = re.search('HOST=(.*)', contents)
18 host = match.group(1)
19 match = re.search('PORT=(.*)', contents)
20 port = match.group(1)
21
22 return {'db': dbName, 'site_title': title, 'host': host, 'port': int(port)}
server.py
(79 / 25)
  
11#!/usr/bin/python
2
23# Mouchak Server -
34# A Flask Application (http://flask.pocoo.org/)
45
56import flask
67import pymongo
78import bson
9import readConfig
810
911app = flask.Flask(__name__)
1012
13
14config = readConfig.readConfig()
15
1116dbClient = pymongo.MongoClient()
12db = dbClient['mouchak']
13collection = db['content']
17db = dbClient[config['db']]
18siteContent = db['content']
19siteMenu = db['menu']
20if siteMenu.find_one() == None:
21 siteMenu.insert({'customMenu': False})
22
1423# handy reference to otherwise long name
1524bson.ObjId = bson.objectid.ObjectId
1625
26
1727def getContent():
1828 content = []
19 for i in collection.find():
29 for i in siteContent.find():
2030 objId = bson.ObjId(i['_id'])
2131 del(i['_id'])
2232 i['id'] = str(objId)
2333 content.append(i)
24 return content
2534
35 menu = siteMenu.find_one()
36 objId = bson.ObjId(menu['_id'])
37 del(menu['_id'])
38 menu['id'] = str(objId)
2639
40 return {'content': content, 'menu': menu}
41
42
43
2744@app.route('/', methods=['GET'])
2845def index():
29 return flask.render_template('index.html', content=getContent())
46 return flask.render_template('index.html', content=getContent(),
47 title=config['site_title'])
3048
3149
32@app.route('/edit', methods=['GET', 'POST'])
50@app.route('/edit', methods=['GET'])
3351def edit():
34 if flask.request.method == 'GET':
35 return flask.render_template('editor.html', content=getContent())
52 return flask.render_template('editor.html', content=getContent(),
53 title=config['site_title'])
3654
37 elif flask.request.method == 'POST':
38 newpage = flask.request.json
39 print newpage
40 res = collection.insert(newpage)
41 print res
42 return flask.jsonify(status='success')#, content=getContent())
4355
44
45@app.route('/edit/<_id>', methods=['PUT', 'DELETE'])
46def editPage(_id):
56@app.route('/page', methods=['POST'])
57def insertPage():
58 newpage = flask.request.json
59 print newpage
60 res = siteContent.insert(newpage)
61 _id = bson.ObjId(res)
62 newpage['id'] = str(_id)
63 del(newpage['_id'])
64 print newpage
65 # FIXME: handle errors
66 return flask.jsonify(status='ok', page=newpage)
67
68
69@app.route('/page/<_id>', methods=['PUT', 'DELETE'])
70def updatePage(_id):
4771 if flask.request.method == 'PUT':
4872 changedPage = flask.request.json
4973 print changedPage
50 res = collection.update({'_id' : bson.ObjId(_id)},
74 print '======='
75 res = siteContent.update({'_id': bson.ObjId(_id)},
5176 changedPage)
5277 print res
53 #print collection.find({'name': changed['name']})
54 #for i in collection.find({'name': changed['name']}):
55 #print i
56 return flask.jsonify(status='success')#, content=getContent())
78 if res['err'] == None:
79 print changedPage
80 return flask.jsonify(status='ok', page=changedPage)
5781
5882 elif flask.request.method == 'DELETE':
5983 delPage = flask.request.url
6084 print delPage
6185 print _id
62 res = collection.remove({'_id': bson.ObjId(_id)})
86 res = siteContent.remove({'_id': bson.ObjId(_id)})
6387 print res
64 return flask.jsonify(status='success', msg='removed')
88 if res['err'] == None:
89 return flask.jsonify(status='ok')
90 else:
91 return flask.jsonify(error=res['err'], status='error')
6592
6693
94#@app.route('/menu', methods=['POST'])
95#def insertMenu():
96# newmenu = flask.request.json
97# print newmenu
98# res = siteMenu.insert(newmenu)
99# print res
100# return flask.jsonify(status='success')#, content=getContent())
101#
102
103@app.route('/menu/<_id>', methods=['PUT'])
104def updateMenu(_id):
105 if flask.request.method == 'PUT':
106 changedMenu = flask.request.json
107 print changedMenu
108 res = siteMenu.update({'_id': bson.ObjId(_id)}, changedMenu)
109 print res
110 return flask.jsonify(status='ok',menu=changedMenu)
111
112 #elif flask.request.method == 'DELETE':
113 # delMenu = flask.request.url
114 # print delMenu
115 # print _id
116 # res = siteMenu.remove({'_id': bson.ObjId(_id)})
117 # return flask.jsonify(status='deleted')
118
119
67120if __name__ == "__main__":
68 app.run(debug=True, host='0.0.0.0')
121 print config
122 app.run(debug=True, host=config['host'], port=config['port'])
  
9797 border: 1px solid #999;
9898 padding: 20px;
9999 width: 400px;
100 height: 400px;
100 /*height: 400px;*/
101101}
102102#pages {
103103 position: absolute;
106106 border: 1px solid black;
107107 padding: 10px;
108108 width: 300px;
109 height: 80%;
109 /*height: 80%;*/
110110}
111111#page {
112112 position: absolute;
137137#specific-content {
138138 padding: 3px;
139139 margin-bottom: 10px;
140}
141#addPage {
142 margin: 10px 0 0 200px;
140143}
141144
142145/* ==========================================================================
  
9494 ========================================================================== */
9595
9696
97
9897/* ==========================================================================
9998 Helper classes
10099 ========================================================================== */
  
88 events: {
99 'click .pagename .disp': 'showPage',
1010 'click #addPage': 'addPage',
11 'click .pagename .remove': 'removePage'
11 'click .pagename .remove': 'removePage',
12 'click #menu-config': 'showMenu'
1213 },
1314 initialize: function() {
1415 _.bindAll(this);
2020 $('#content-container').append(this.$el);
2121 this.$el.append(this.template());
2222 this.$pagelist = $('#pagelist');
23 var menu = M.site_content.menu;
24 //console.log(menu);
25 this.menuconfig = new M.types.model.menu(menu);
26 this.menuconfigview = new MenuConfigView({model: this.menuconfig});
2327 },
2428 render: function() {
2529 // append the page list
4242 M.editor.pageview = pageview;
4343 },
4444 addPage: function() {
45 var newpage = new Page();
45 var newpage = new M.types.model.Page();
4646 M.pages.add(newpage);
4747 var newpageview = new PageView({model: newpage});
4848 newpageview.render();
6464 console.log('failed', model, xhr);
6565 }
6666 });
67 }
68 });
69
70 var Page = Backbone.Model.extend({
71 defaults: {
72 name: '',
73 title: '',
74 children: [],
75 content: []
7667 },
77 initialize: function() {
78 this.id = this.get('id');
68 showMenu: function(event) {
69 this.menuconfigview.render();
7970 }
8071 });
8172
8273 var Pages = Backbone.Collection.extend({
83 model: Page,
84 url: '/edit'
74 model: M.types.model.Page,
75 url: '/page'
8576 });
8677
8778 /* view to manage each page and their properties - change page properties,
111111 console.log('name changed', page);
112112 },
113113 render: function() {
114 console.log(this.$el);
115114 $('#page').html('');
116 console.log('content: ', this.model.get('content'));
115 //console.log('content: ', this.model.get('content'));
117116
118117 this.$el.html(this.template({
119118 name: this.model.get('name'),
120119 title: this.model.get('title'),
121120 children: this.model.get('children'),
122 content: this.listContent()
121 content: this.listContent(),
122 checked: this.model.get('showNav') ? 'checked="checked"' : ''
123123 }));
124124
125125 //hover effect
128128 }, function(event) {
129129 $(event.target).closest('.content-item').removeClass('alert-error')
130130 });
131 console.log('done');
132131 },
133132 listContent: function() {
134133 var content = '';
182182 console.log('recvd remove event..about to process..');
183183 var content = this.model.get('content');
184184 var idx = $(event.target).parent().attr('for');
185 idx = Number(idx);
185 idx = Number(idx); //is this a correct way of doing it?
186186 console.log('remove content: ', content[idx]);
187187 content.splice(idx, 1);
188188 this.model.set({'content': content});
197197 children = (children === '') ? [] : children.split(',');
198198 this.model.set({'name': name, 'title': title, 'children': children});
199199
200 if($('#showNav').is(':checked')) {
201 this.model.set({'showNav': true});
202 }
203 else {
204 this.model.set({'showNav': false});
205 }
206
200207 this.model.save({}, {
201208 success: function(model, response) {
202209 console.log('saved', model, response);
210 model.set(response.page);
211 model.id = response.page.id;
203212 },
204213 error: function(model, xhr) {
205214 console.log('failed', model, xhr);
325325
326326 /* view to configure custom navigation menu */
327327 var MenuConfigView = Backbone.View.extend({
328 el: '#menu-config',
328 tagName: 'div',
329 id: 'page',
329330 events: {
331 'change #custom-menu': 'customMenuChange',
332 'click #updateMenu': 'saveMenu'
330333 },
331334 initialize: function() {
332335 _.bindAll(this);
333336 this.template = _.template($('#menu-config-template').html());
334337 },
335338 render: function() {
336 $('#content-container').append(this.template({
337 menu: 'foo'
339 $('#page').remove();
340 $('#content-container').append(this.$el);
341 console.log('rendering..', this.$el);
342 this.$el.html(this.template({
343 pos: this.model.get('pos'),
344 menu: this.model.get('html')
338345 }));
346 this.$menuOptions = $('.menu-options');
347
348 if(this.model.get('customMenu') === true) {
349 $('#custom-menu').attr('checked', true);
350 this.$menuOptions.show();
351 }
352 },
353 showMenuOptions: function(bool) {
354 if(bool === true) {
355 this.$menuOptions.show();
356 }
357 else {
358 this.$menuOptions.hide();
359 }
360 },
361 customMenuChange: function(event) {
362 this.$menuOptions = $('.menu-options');
363 if($('#custom-menu').is(':checked')) {
364 this.model.set({'customMenu': true});
365 }
366 else {
367 this.model.set({'customMenu': false});
368 }
369 this.showMenuOptions(this.model.get('customMenu'));
370 },
371 saveMenu: function() {
372 var menuHTML = $('#menu').val().trim();
373 this.model.set({'html': menuHTML});
374 console.log(this.model.toJSON());
375 this.model.save({}, {
376 success: function(model, response) {
377 },
378 error: function(xhr, response) {
379 }
380 });
339381 }
340382 });
341383
342384 M.editor = {
343385 init: function() {
344 M.pages = new Pages(M.site_content);
386 M.pages = new Pages(M.site_content.content);
345387 var pagelistview = new PageListView();
346388 pagelistview.render();
347389 M.pages.on('add', function(page) {
348390 pagelistview.render();
349391 });
350392 M.pagelistview = pagelistview;
351
352 //var menuconfig = new MenuConfigView();
353 //menuconfig.render();
354393 }
355394 };
356395
357 function escapeHtml(string) {
396 var escapeHtml = function(string) {
358397 var entityMap = {
359398 "&": "&amp;",
360399 "<": "&lt;",
403403 "/": '&#x2F;'
404404 };
405405 return String(string).replace(/[&<>"'\/]/g, function (s) {
406 return entityMap[s];
407 });
406 return entityMap[s];
407 });
408408 }
409409
410410})(M);
  
8686 // model for each Page
8787 var Page = Backbone.Model.extend({
8888 defaults: {
89 name: "index",
89 name: "",
9090 title: "",
9191 children: [],
92 content: []
92 content: [],
93 showNav: true
9394 },
9495 initialize: function() {
95 // adding the name of the model as its id.
96 // look up of this model through the collection
97 // is faster this way.
98 //this.set({id: M.sanitize(this.get('name'))});
96 this.id = this.get('id');
9997 this.set({id: this.get('id')});
10098 }
10199 });
102102 model: Page
103103 });
104104
105 var Menu = Backbone.Model.extend({
106 defaults: {
107 customMenu: false
108 },
109 url: function() {
110 return '/menu/' + this.id;
111 },
112 initialize: function() {
113 this.id = this.get('id');
114 },
115 });
116
105117 //export types to the typemap
106118 M.types = M.types || {};
107119 M.types.model = {
108120 'text': Text,
109121 'image': Image,
110122 'video': Video,
123 'menu': Menu,
111124 'rss': RSS,
112125 'table': Table,
113126 'plugin': Plugin,
  
1313 _.bindAll(this);
1414 },
1515 render: function() {
16 var navview = new NavigationView();
16 var menu = new M.types.model.menu(M.site_content.menu);
17 var navview = new NavigationView({model: menu});
1718 navview.render();
1819 },
1920 updateBreadcrumbs: function(event) {
3333 this.template = _.template($('#nav-bar-template').html());
3434 },
3535 render: function() {
36 this.$el.append(this.template({}));
37 this.$ul = $('.nav');
38 this.populate();
36 // if custom menu is not defined, render a default menu
37 console.log(this.model.toJSON());
38 if(this.model.get('customMenu') === false) {
39 console.log('generating default menu..');
40 this.$el.append(this.template({}));
41 this.$ul = $('.nav');
42 this.populate();
43 }
44 // else render the custom menu
45 else {
46 console.log('rendering custom menu..');
47 this.$el.append(this.model.get('html'));
48 }
3949 },
4050 populate: function() {
4151 var item_template = _.template($('#nav-item-template').html());
4252 _.each(M.pages.models, function(page) {
43 console.log(_.isEmpty(page.get('children')));
53 //console.log('no children?', _.isEmpty(page.get('children')));
4454 this.$ul.append(item_template({
4555 cls: (_.isEmpty(page.get('children'))) ? '' : 'dropdown',
4656 page: page.get('name')
8585 M.rss_view.render();
8686 }
8787 var id = nameIdMap[page];
88 if(!id) {
89 this.render404();
90 return;
91 }
8892 $('#'+id).show();
8993 $('.'+page).show();
94 if(M.pages.get(id).get('showNav') === false) {
95 $('#navigation').hide();
96 }
97 else {
98 $('#navigation').show();
99 }
100 },
101 render404: function() {
102 $('.pageview').hide();
103 var notFound = "Sorry, a page corresponding to your URL was not found.\n" +
104 "Maybe you have typed the URL wrong, or this page is no longer available.";
105 alert(notFound);
90106 }
91107});
92108
115115 M.tags = {}; //global tag cache
116116
117117 // global collection of pages
118 M.pages = new types.model.Pages(M.site_content);
118 M.pages = new types.model.Pages(M.site_content.content);
119119
120120 // iterate through pages to get their content and render them using views and
121121 // models
  
66 <head>
77 <meta charset="utf-8">
88 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
9 <title> Mouchak - ummm!</title>
9 <title> Editing.. | {{title}} </title>
1010 <meta name="description" content="">
1111 <meta name="viewport" content="width=device-width">
1212
2424
2525 <div class="container" id="container">
2626 <div id="header"> </div>
27
2827 <div id="content-container"></div>
29
3028 <div id="footer">
3129 </div>
3230 </div>
3331
34
3532 <script>
3633 // initialize editor
3734 window.M = window.M || {};
5151
5252 <!-- Underscore templates -->
5353 <script type="text/template" id="menu-config-template">
54 <div id="menu-config">
55 <textarea id="menu">
56 <%= menu %>
57 </textarea>
54 <div class="page">
55 <div class="menu-config">
56 <h4> Menu Config </h4>
57 <form class="form-horizontal">
58 <div class="control-group">
59 <input id="custom-menu" type="checkbox">
60 <span class=""><strong> Custom Menu </strong></span>
61 </div>
62 <button id="updateMenu" type="submit" class="btn btn-primary pull-right"> Update </button>
63 <div class="clearfix"></div>
64 <div class="menu-options" style="display: none;">
65 <div class="control-group">
66 <div class="input-prepend">
67 <span class="add-on"> <strong> Position </strong></span>
68 <input id="pos" type="text" placeholder="[left, top]"
69 value="<%= pos %>">
70 </div>
71 </div>
72 <label><strong> HTML </strong></label>
73 <textarea id="menu">
74 <%= menu %>
75 </textarea>
76 </div>
77 </form>
78 </div>
5879 </div>
5980 </script>
6081
8383 <div id="pagelistview">
8484 <h4> List of Pages </h4>
8585 <div id="pagelist"></div>
86 <button class="btn btn-primary pull-right" id="addPage">Add Page</button>
87 <p><a href="/" class="btn"> Go to site </a></p>
86 <button class="btn btn-primary" id="addPage">Add Page</button>
87 <hr>
88 <p> <a href="javascript:void(0);" id="menu-config"> Site Menu </a> </p>
89 <p><a href="/"> Go to site </a></p>
8890 </div>
8991 </script>
92
9093 <script type="text/template" id="page-list-item-template">
9194 <div class="pagename">
92 <a class="disp" id="<%= id %>" href="#"><%= name %></a>
95 <a class="disp" id="<%= id %>" href="javascript:void(0);"><%= name %></a>
9396 <span class="pull-right">
94 <a href="#" class="remove" for="<%= id %>">
97 <a href="javascript:void(0);" class="remove" for="<%= id %>">
9598 <i class="icon-trash"></i>
9699 </a>
97100 </span>
101 <span class="clearfix"></span>
98102 </div>
99103 </script>
100104
126126 value="<%= children %>">
127127 </div>
128128 </div>
129 <div class="control-group">
130 <div class="input-prepend">
131 <span class="add-on"><strong> Show Navigation </strong></span>
132 <input id="showNav" type="checkbox" <%=checked%>>
133 </div>
134 </div>
129135 <label><strong> Content </strong></label>
130136 <div id="content" class="content well">
131137 <%= content %>
139139 <button class="addContent btn btn-mini btn-primary">Add Content</button>
140140 </div>
141141 <button id="updatePage" type="submit" class="btn btn-primary pull-right"> Update </button>
142 <div class="clearfix"></div>
142143 </form>
143144 </div>
144145 </script>
153153 </span>
154154 </span>
155155 <span class="pull-right">
156 <a href="#" class="remove" for="<%=no%>"><i class="icon-trash"></i></a>
156 <a href="javascript:void(0);" class="remove" for="<%=no%>"><i class="icon-trash"></i></a>
157157 </span>
158 <span class="clearfix"></span>
158159 </div>
159160 </script>
160161
  
66 <head>
77 <meta charset="utf-8">
88 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
9 <title> Mouchak - ummm!</title>
9 <title> {{title}} </title>
1010 <meta name="description" content="">
1111 <meta name="viewport" content="width=device-width">
1212