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