1
#!/usr/bin/python
2
3
# Mouchak Server -
4
# A Flask Application (http://flask.pocoo.org/)
5
6
###############################################################################
7
# TODO: While moving rendering, routing and controlling to server side, the API
8
# has to change and improve. The API should be as follows:-
9
# --
10
# For Pages
11
#
12
# [GET] /mouchak/api/v1a/pages/ - Retrieve list of all the pages
13
# [GET] /mouchak/api/v1a/pages/<id> - Retrieve specfic page
14
# [POST] /mouchak/api/v1a/pages - Create a new page, with data in payload
15
# [PUT] /mouchak/api/v1a/pages/<id> - Update a specific page, with data in
16
# payload
17
# [DELETE] /mouchak/api/v1a/pages/<id> - Delete a specific page
18
# --
19
#
20
# For Sitemap (There is only one instance of sitemap in a given website, hence
21
# the API is quite simple.
22
#
23
# [GET] /mouchak/api/v1a/sitemap - Retrieve the sitemap
24
# [PUT] /mouchak/api/v1a/sitemap - Update the sitemap
25
###############################################################################
26
27
import flask
28
import pymongo
29
import bson
30
import conf
31
import os
32
import json
33
from werkzeug import secure_filename
34
from datetime import datetime
35
36
PLUGIN_UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__))
37
                                    + '/static/user_plugins')
38
39
ALLOWED_EXTENSIONS = set(['js', 'css', 'jpg', 'JPG', 'png', 'gif', 'PNG',
40
                          'svg', 'pdf'])
41
#ALLOWED_EXTENSIONS = set(['js', 'css'])
42
43
FILE_UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)) +
44
                                  '/static/uploads')
45
46
app = flask.Flask(__name__)
47
48
49
50
dbClient = pymongo.MongoClient()
51
52
db = dbClient[conf.DB]
53
siteContent = db['content']
54
siteMenu = db['menu']
55
lastUpdated = db['last_updated']
56
analytics_coll = db['analytics']
57
58
if siteMenu.find_one() == None:
59
    siteMenu.insert({'customMenu': False, 'menuOrder': [], 'html': ''})
60
61
siteFooter = db['footer']
62
if siteFooter.find_one() == None:
63
    siteFooter.insert({'html': ''})
64
65
siteHeader = db['header']
66
if siteHeader.find_one() == None:
67
    siteHeader.insert({'html': ''})
68
69
70
# handy reference to otherwise long name
71
bson.ObjId = bson.objectid.ObjectId
72
73
74
def getContent():
75
    content = []
76
    for i in siteContent.find():
77
        objId = bson.ObjId(i['_id'])
78
        del(i['_id'])
79
        i['id'] = str(objId)
80
        content.append(i)
81
82
    menu = siteMenu.find_one()
83
    objId = bson.ObjId(menu['_id'])
84
    del(menu['_id'])
85
    menu['id'] = str(objId)
86
87
    footer = siteFooter.find_one()
88
    objId = bson.ObjId(footer['_id'])
89
    del(footer['_id'])
90
    footer['id'] = str(objId)
91
92
    header = siteHeader.find_one()
93
    objId = bson.ObjId(header['_id'])
94
    del(header['_id'])
95
    header['id'] = str(objId)
96
97
    last_updated = lastUpdated.find_one()
98
    last_updated_date = last_updated['last_updated']
99
100
    return {'content': content, 'menu': menu, 'footer': footer, 'header':
101
            header, 'last_updated': last_updated_date.isoformat()}
102
103
104
def allowed_file(filename):
105
    return '.' in filename and \
106
            filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS
107
108
109
@app.errorhandler(404)
110
def pageNotFound(e):
111
    return flask.render_template('404.html'), 404
112
113
114
@app.route('/', methods=['GET'])
115
def index():
116
    return flask.render_template('index.html', content=getContent(),
117
                                 title=conf.SITE_TITLE)
118
119
120
@app.route('/edit', methods=['GET'])
121
def edit():
122
    if "logged_in" in flask.session:
123
        flask.session['key'] = conf.SECRET_KEY
124
        return flask.render_template('editor.html', content=getContent(),
125
                                     title=conf.SITE_TITLE)
126
    else:
127
        return flask.redirect(flask.url_for('login'))
128
129
130
@app.route('/pages', methods=['GET'])
131
def listPages():
132
    # if limit and offset are given as query string params,
133
    # send pages with that limit starting from the offset
134
    if flask.request.args.get('limit'):
135
        content = []
136
        limit = int(flask.request.args.get('limit'))
137
        if flask.request.args.get('offset'):
138
            offset = int(flask.request.args.get('offset'))
139
        else:
140
            offset = 0
141
        for page in siteContent.find().sort('_id', 1)[offset:offset+limit]:
142
            del(page['_id'])
143
            content.append(page)
144
        #print len(content)
145
        return flask.make_response(json.dumps(content), '200 OK',
146
                                   {'Content-Type': 'application/json'})
147
    else:
148
        content = getContent()['content']
149
        return flask.make_response(json.dumps(content), '200 OK',
150
                               {'Content-Type': 'application/json'})
151
152
153
@app.route('/pages/<_id>', methods=['GET'])
154
def listPage(_id):
155
    try:
156
        page = siteContent.find_one({'_id': bson.ObjId(_id)})
157
        del(page['_id'])
158
        #print page
159
        return flask.jsonify(page)
160
    except:
161
        return flask.abort(404)
162
163
164
@app.route('/page', methods=['POST'])
165
def insertPage():
166
    newpage = flask.request.json
167
    #print newpage
168
    res = siteContent.insert(newpage)
169
    _id = bson.ObjId(res)
170
    newpage['id'] = str(_id)
171
    del(newpage['_id'])
172
    #print newpage
173
    # FIXME: handle errors
174
    #return flask.jsonify(status='ok', page=newpage)
175
    return flask.jsonify(newpage)
176
177
178
@app.route('/page/<_id>', methods=['PUT', 'DELETE'])
179
def updatePage(_id):
180
    if flask.request.method == 'PUT':
181
        changedPage = flask.request.json
182
        print changedPage
183
        print '======='
184
        res = siteContent.update({'_id': bson.ObjId(_id)},
185
                                changedPage)
186
        print res
187
        if res['err'] == None:
188
            print changedPage
189
            #return flask.jsonify(status='ok', page=changedPage)
190
            return flask.jsonify(changedPage)
191
192
    elif flask.request.method == 'DELETE':
193
        delPage = flask.request.url
194
        print delPage
195
        print _id
196
        res = siteContent.remove({'_id': bson.ObjId(_id)})
197
        print res
198
        if res['err'] == None:
199
            return flask.jsonify(status='ok')
200
        else:
201
            return flask.jsonify(error=res['err'], status='error')
202
203
204
@app.route('/footer', methods=['POST'])
205
def insertFooter():
206
    return '200 OK'
207
208
@app.route('/footer/<_id>', methods=['PUT'])
209
def updateFooter(_id):
210
    if flask.request.method == 'PUT':
211
        changedFooter = flask.request.json
212
        print "changed footer:"
213
        print changedFooter
214
        res = siteFooter.update({'_id': bson.ObjId(_id)}, changedFooter)
215
        print res
216
        return flask.jsonify(changedFooter)
217
218
@app.route('/header', methods=['POST'])
219
def insertHeader():
220
    return '200 OK'
221
222
@app.route('/header/<_id>', methods=['PUT'])
223
def updateHeader(_id):
224
    if flask.request.method == 'PUT':
225
        changedHeader = flask.request.json
226
        print "changed header:"
227
        print changedHeader
228
        res = siteHeader.update({'_id': bson.ObjId(_id)}, changedHeader)
229
        print res
230
        return flask.jsonify(changedHeader)
231
232
233
@app.route('/menu', methods=['POST'])
234
def insertMenu():
235
    #newmenu = flask.request.json
236
    #print newmenu
237
    #res = siteMenu.insert(newmenu)
238
    #print res
239
    #return flask.jsonify(status='success')#, content=getContent())
240
    return '200 OK'
241
242
243
@app.route('/menu/<_id>', methods=['PUT'])
244
def updateMenu(_id):
245
    if flask.request.method == 'PUT':
246
        changedMenu = flask.request.json
247
        print "changed menu:"
248
        print changedMenu
249
        res = siteMenu.update({'_id': bson.ObjId(_id)}, changedMenu)
250
        print res
251
        #return flask.jsonify(status='ok', menu=changedMenu)
252
        return flask.jsonify(changedMenu)
253
254
    #elif flask.request.method == 'DELETE':
255
    #    delMenu = flask.request.url
256
    #    print delMenu
257
    #    print _id
258
    #    res = siteMenu.remove({'_id': bson.ObjId(_id)})
259
    #    return flask.jsonify(status='deleted')
260
261
262
# Basic login for one single admin user whose credentials are in conf.py
263
@app.route('/login', methods=['GET', 'POST'])
264
def login():
265
    error = None
266
    if flask.request.method == 'POST':
267
        if flask.request.form['username'] != conf.ADMIN_USERNAME:
268
            error = 'Invalid username'
269
        elif flask.request.form['password'] != conf.ADMIN_PASSWORD:
270
            error = 'Invaid password'
271
        else:
272
            flask.session['logged_in'] = True
273
            flask.session['key'] = conf.SECRET_KEY
274
            flask.flash('You were logged in')
275
            return flask.redirect(flask.url_for('edit'))
276
    return flask.render_template('login.html', error=error)
277
278
@app.route('/logout')
279
def logout():
280
    flask.session.pop('logged_in', None)
281
    flask.flash('You were logged out')
282
    return flask.redirect(flask.url_for('login'))
283
284
285
#TODO: refactor these code to classes
286
#TODO: find out if this is a good method for saving plugins..
287
@app.route('/static/user_plugins/<filename>', methods=['POST'])
288
def savePlugin(filename):
289
    if flask.request.method == 'POST':
290
        if filename and allowed_file(filename):
291
            data = flask.request.form['code']
292
            filename = secure_filename(filename)
293
            fh = open(os.path.join(PLUGIN_UPLOAD_FOLDER + '/' + filename), 'w')
294
            fh.write(data)
295
            fh.close()
296
            return flask.jsonify(saved = True)
297
298
#TODO: find out if this is a good method for uploading plugins..
299
@app.route('/upload/plugin', methods=['POST'])
300
def uploadPlugin():
301
    if flask.request.method == 'POST':
302
        print flask.request.files
303
        file = flask.request.files['plugin-file']
304
        if file and allowed_file(file.filename):
305
            filename = secure_filename(file.filename)
306
            file.save(os.path.join(app.config['PLUGIN_UPLOAD_FOLDER'],
307
                                   filename))
308
309
            #return flask.redirect(flask.url_for('uploaded_file',
310
            #            filename=filename))
311
            return flask.jsonify(uploaded = True,
312
                                 path=flask.url_for('static', filename =\
313
                                                    'user_plugins/'+ filename))
314
315
@app.route('/upload', methods=['GET', 'POST'])
316
def upload():
317
    if flask.request.method == 'POST':
318
        print flask.request.files
319
        file = flask.request.files['upload-file']
320
        if file and allowed_file(file.filename):
321
            print 'file ok'
322
            filename = secure_filename(file.filename)
323
            file.save(os.path.join(app.config['FILE_UPLOAD_FOLDER'], filename))
324
325
            return flask.jsonify(uploaded = True, path =\
326
                                 flask.url_for('static', filename =\
327
                                               'uploads/' + filename))
328
329
        else:
330
            resp = flask.make_response()
331
            print 'file not ok'
332
            resp.status_code = 400
333
            return resp
334
335
    if flask.request.method == 'GET':
336
        uploaded_files = os.listdir(app.config['FILE_UPLOAD_FOLDER'])
337
        print uploaded_files
338
        return flask.jsonify({'uploaded_files': uploaded_files})
339
340
@app.route('/upload/<filename>', methods=['DELETE'])
341
def removeFile(filename):
342
    filepath = os.path.join(app.config['FILE_UPLOAD_FOLDER'], filename)
343
    print filepath
344
    res = os.remove(filepath)
345
    print res
346
    return '200 OK'
347
348
# KHMDL needs analytics. So adding analytics to their website.
349
@app.route('/analytics', methods=['GET', 'POST'])
350
def analytics():
351
    response = flask.make_response()
352
    if flask.request.method == 'GET':
353
        #TODO: gather analytics data from db and send back a HTML rendering it
354
        pass
355
    elif flask.request.method == 'POST':
356
        if 'type' not in flask.request.form:
357
            abort(400)
358
359
        data = {}
360
        data['type'] = flask.request.form['type']
361
        if data['type'] == 'pageview':
362
            data['page'] = flask.request.form['page']
363
364
        analytics_coll.insert(data)
365
        total_hits = analytics_coll.find({'type': 'pageview'}).count()
366
367
    return flask.jsonify(total_hits=total_hits)
368
369
@app.route('/updated-content', methods=['POST'])
370
def updateLastUpdated():
371
    if "logged_in" not in flask.session:
372
        abort(403)
373
374
    response = flask.make_response()
375
    last_updated = lastUpdated.find_one()
376
    if last_updated == None:
377
        lastUpdated.insert({'last_updated': datetime.utcnow()})
378
    else:
379
        lastUpdated.update({'_id': last_updated['_id']},
380
                                 {'last_updated': datetime.utcnow()})
381
382
    response.status_code = 200
383
    return response
384
385
386
@app.route('/robots.txt')
387
@app.route('/crossdomain.xml')
388
def static_from_root():
389
    return flask.send_from_directory(app.static_folder, request.path[1:])
390
391
392
app.config.from_object(conf)
393
app.config['PLUGIN_UPLOAD_FOLDER'] = PLUGIN_UPLOAD_FOLDER
394
app.config['FILE_UPLOAD_FOLDER'] = FILE_UPLOAD_FOLDER
395
396
import logging,os
397
from logging import FileHandler
398
399
fil = FileHandler(os.path.join(os.path.dirname(__file__),'logme'),mode='a')
400
fil.setLevel(logging.ERROR)
401
app.logger.addHandler(fil)
402
403
404
405
if __name__ == "__main__":
406
    app.run(debug=True, host=conf.HOST, port=conf.PORT)