Commit 1d593744cace6fa44757c77b06803b5738707889

Add uploads support

  Add basic uploads support. This should improve both in terms of UI and in
terms of robustness, and functionality.
  • Diff rendering mode:
  • inline
  • side by side

mouchak/server.py

3434
35PLUGIN_UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__))35PLUGIN_UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__))
36 + '/static/user_plugins')36 + '/static/user_plugins')
37PLUGIN_ALLOWED_EXTENSIONS = set(['js', 'css'])
3837
38ALLOWED_EXTENSIONS = set(['js', 'css', 'jpg', 'JPG', 'png', 'gif', 'PNG',
39 'svg', 'pdf'])
40#ALLOWED_EXTENSIONS = set(['js', 'css'])
41
39FILE_UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)) +42FILE_UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)) +
40 '/static/uploads')43 '/static/uploads')
4144
9595
96def allowed_file(filename):96def allowed_file(filename):
97 return '.' in filename and \97 return '.' in filename and \
98 filename.rsplit('.', 1)[1] in PLUGIN_ALLOWED_EXTENSIONS
98 filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS
9999
100100
101@app.errorhandler(404)101@app.errorhandler(404)
302 #return flask.redirect(flask.url_for('uploaded_file',302 #return flask.redirect(flask.url_for('uploaded_file',
303 # filename=filename))303 # filename=filename))
304 return flask.jsonify(uploaded = True,304 return flask.jsonify(uploaded = True,
305 path=flask.url_for('static', filename =
305 path=flask.url_for('static', filename =\
306 'user_plugins/'+ filename))306 'user_plugins/'+ filename))
307307
308@app.route('/upload', methods=['GET', 'POST'])
309def upload():
310 if flask.request.method == 'POST':
311 print flask.request.files
312 file = flask.request.files['upload-file']
313 if file and allowed_file(file.filename):
314 print 'file ok'
315 filename = secure_filename(file.filename)
316 file.save(os.path.join(app.config['FILE_UPLOAD_FOLDER'], filename))
317
318 return flask.jsonify(uploaded = True, path =\
319 flask.url_for('static', filename =\
320 'uploads/' + filename))
321
322 else:
323 resp = flask.make_response()
324 print 'file not ok'
325 resp.status_code = 400
326 return resp
327
328 if flask.request.method == 'GET':
329 uploaded_files = os.listdir(app.config['FILE_UPLOAD_FOLDER'])
330 print uploaded_files
331 return flask.jsonify({'uploaded_files': uploaded_files})
332
333@app.route('/upload/<filename>', methods=['DELETE'])
334def removeFile(filename):
335 filepath = os.path.join(app.config['FILE_UPLOAD_FOLDER'], filename)
336 print filepath
337 res = os.remove(filepath)
338 print res
339 return '200 OK'
340
341
308@app.route('/robots.txt')342@app.route('/robots.txt')
309@app.route('/crossdomain.xml')343@app.route('/crossdomain.xml')
310def static_from_root():344def static_from_root():
347347
348app.config.from_object(conf)348app.config.from_object(conf)
349app.config['PLUGIN_UPLOAD_FOLDER'] = PLUGIN_UPLOAD_FOLDER349app.config['PLUGIN_UPLOAD_FOLDER'] = PLUGIN_UPLOAD_FOLDER
350app.config['FILE_UPLOAD_FOLDER'] = FILE_UPLOAD_FOLDER
350351
351import logging,os352import logging,os
352from logging import FileHandler353from logging import FileHandler

mouchak/static/js/editor.js

11 'click .pagename .remove': 'removePage',11 'click .pagename .remove': 'removePage',
12 'click #menu-config': 'showMenu',12 'click #menu-config': 'showMenu',
13 'click #footer-config': 'showFooterConfig',13 'click #footer-config': 'showFooterConfig',
14 'click #header-config': 'showHeaderConfig'
14 'click #header-config': 'showHeaderConfig',
15 'click #uploads': 'uploads'
15 },16 },
16 initialize: function() {17 initialize: function() {
17 _.bindAll.apply(_, [this].concat(_.functions(this)));18 _.bindAll.apply(_, [this].concat(_.functions(this)));
31 this.footerconfigview = new FooterConfigView({model: this.footerconfig});31 this.footerconfigview = new FooterConfigView({model: this.footerconfig});
32 this.headerconfig = new M.types.model.header(M.site_content.header);32 this.headerconfig = new M.types.model.header(M.site_content.header);
33 this.headerconfigview = new HeaderConfigView({model: this.headerconfig});33 this.headerconfigview = new HeaderConfigView({model: this.headerconfig});
34 this.uploadview = new UploadView();
34 },35 },
35 render: function() {36 render: function() {
36 // append the page list37 // append the page list
104 this.headerconfigview.render();104 this.headerconfigview.render();
105 return false;105 return false;
106 },106 },
107 uploads: function(event) {
108 event.preventDefault();
109 this.uploadview.render();
110 return false;
111 },
107 // validate the page list with menu order list112 // validate the page list with menu order list
108 validate: function() {113 validate: function() {
109 //TODO: validate if the menu order list matches with the list of pages114 //TODO: validate if the menu order list matches with the list of pages
676 ' will be rendered'+676 ' will be rendered'+
677 '\n@delay: (optional) a delay time, after which the notification'+677 '\n@delay: (optional) a delay time, after which the notification'+
678 ' will be hidden';678 ' will be hidden';
679 }
680 });
681
682 /* Upload View */
683 var UploadView = Backbone.View.extend({
684 tagName: 'div',
685 className: 'prettybox-lg',
686 id: 'page',
687 events: {
688 'click #upload-new-file': 'uploadFile',
689 'click .uploaded-item .remove': 'removeFile'
690 },
691 initialize: function() {
692 _.bindAll.apply(_, [this].concat(_.functions(this)));
693 this.template = _.template($('#uploads-template').html());
694 },
695 render: function() {
696 $('#page').remove();
697 $('#content-container').append(this.$el);
698 //console.log('rendering..', this.$el);
699 var uploaded_files, self = this;
700 M.editor.showOverlay();
701 $.ajax({
702 url: '/upload',
703 type: 'GET',
704 success: function(data) {
705 M.editor.hideOverlay();
706 self.$el.html(self.template({
707 }));
708 self.appendFileListTemplate(data.uploaded_files);
709 self.delegateEvents();
710 }
711 });
712 },
713 appendFileListTemplate: function(files) {
714 var template = _.template($('#uploaded-item-template').html());
715 if(files.length) {
716 _.each(files, function(file) {
717 $('#uploads-list').append(template({
718 filename: file
719 }));
720 });
721 }
722 else {
723 $('#uploads-list').html('<b> No files uploaded yet </b>');
724 }
725 },
726 uploadFile: function() {
727 //console.log('upload file');
728 var self = this;
729 M.editor.showOverlay();
730 var $form = $('#file-upload-form')[0];
731 var formdata = new FormData($form);
732 $.ajax({
733 type: 'POST',
734 url: '/upload',
735 data: formdata,
736 processData: false,
737 contentType: false,
738 success: function(response) {
739 M.editor.hideOverlay();
740 M.editor.notifs.show('success', 'Success', 'File uploaded');
741 self.render();
742 },
743 error: function(jqxhr, status, error) {
744 M.editor.hideOverlay();
745 if(error === 'BAD REQUEST') {
746 var msg = 'File format not allowed. Please contact your administrator to allow this kind of file.'
747 } else {
748 var msg = 'Something went wrong. Please try again!';
749 }
750 M.editor.notifs.show('fail', 'Error!', msg);
751 }
752 });
753 },
754 removeFile: function(event) {
755 M.editor.showOverlay();
756 //console.log('remove file');
757 var self = this;
758 var filename = $(event.currentTarget).attr('for');
759 $.ajax({
760 type: 'DELETE',
761 url: '/upload/' + filename,
762 success: function(data) {
763 M.editor.hideOverlay();
764 self.render();
765 },
766 error: function(jqxhr, status, error) {
767 console.log(arguments);
768 }
769 });
679 }770 }
680 });771 });
681772

mouchak/templates/editor.html

26 M.HeaderURL = function() { return "{{ url_for('insertHeader') }}"; };26 M.HeaderURL = function() { return "{{ url_for('insertHeader') }}"; };
27 M.PageURL = function() { return "{{ url_for('insertPage') }}"; };27 M.PageURL = function() { return "{{ url_for('insertPage') }}"; };
28 M.PluginUploadURL = function() { return "{{ url_for('uploadPlugin') }}"; };28 M.PluginUploadURL = function() { return "{{ url_for('uploadPlugin') }}"; };
29 M.UploadsURL = function() { return "{{ url_for('static', filename='uploads/') }}"; };
29 M.site_content = {{ content|tojson|safe }};30 M.site_content = {{ content|tojson|safe }};
30 window.onload = function() {31 window.onload = function() {
31 M.editor.init();32 M.editor.init();
144 <p> <a href="#" id="header-config"> Header </a> </p>144 <p> <a href="#" id="header-config"> Header </a> </p>
145 <p> <a href="#" id="footer-config"> Footer </a> </p>145 <p> <a href="#" id="footer-config"> Footer </a> </p>
146 <p> <a href="#" id="menu-config"> Navigation Menu </a> </p>146 <p> <a href="#" id="menu-config"> Navigation Menu </a> </p>
147 <p> <a href="#" id="uploads"> Uploads </a> </p>
147 <p><a href="{{ url_for('index') }}"> Go to site </a></p>148 <p><a href="{{ url_for('index') }}"> Go to site </a></p>
148 </div>149 </div>
149 </script>150 </script>
196 <small>title of the page</small>196 <small>title of the page</small>
197 </span>197 </span>
198 </div>198 </div>
199 <div class="form-group">
199 <!--div class="form-group">
200 <div class="input-group">200 <div class="input-group">
201 <span class="input-group-addon"> <strong>Children</strong> </span>201 <span class="input-group-addon"> <strong>Children</strong> </span>
202 <input class="form-control" id="children" type="text" placeholder="csv of child pages"202 <input class="form-control" id="children" type="text" placeholder="csv of child pages"
205 <span class="help-block">205 <span class="help-block">
206 <small> leave this blank for now</small>206 <small> leave this blank for now</small>
207 </span>207 </span>
208 </div>
208 </div-->
209 <div class="form-group">209 <div class="form-group">
210 <div class="input-group">210 <div class="input-group">
211 <span class="input-group-addon">211 <span class="input-group-addon">
336 <div class="row">336 <div class="row">
337 <div class="form-group col-lg-6">337 <div class="form-group col-lg-6">
338 <form id="plugin-upload-form" action="" method="post" enctype="multipart/form-data">338 <form id="plugin-upload-form" action="" method="post" enctype="multipart/form-data">
339 <input type="file" name="plugin-file" id="select-plugin">
339 <input type="file" name="plugin-file" id="select-plugin">
340 <input type="button" id="upload-plugin" class="btn btn-info" value="Upload Plugin">340 <input type="button" id="upload-plugin" class="btn btn-info" value="Upload Plugin">
341 </form>341 </form>
342 </div>342 </div>
404 <button type="button" class="close" data-dismiss="alert">&times;</button>404 <button type="button" class="close" data-dismiss="alert">&times;</button>
405 <h4> <%= title %> </h4>405 <h4> <%= title %> </h4>
406 <%= msg %>406 <%= msg %>
407 </div>
408 </script>
409
410 <script type="text/template" id="uploads-template">
411 <div class="page">
412 <div><h4> All your uploads </h4></div>
413 <div id="uploads-list" class="well">
414 </div>
415 <hr>
416 <div class="row">
417 <div> <h4> New Upload </h4> </div>
418 <form id="file-upload-form" action="" method="post" enctype="multipart/form-data">
419 <input type="file" name="upload-file" id="select-file">
420 <input type="button" id="upload-new-file" class="btn btn-sm btn-primary update-btn" value="Upload File">
421 </form>
422 </div>
423 <div class="clearfix"></div>
424 </div>
425 </script>
426
427 <script type="text/template" id="uploaded-item-template">
428 <div class="uploaded-item row">
429 <span> &lt;icon here&gt; </span>
430 <!--span class="uploaded-item-name"> <%= filename %> </span-->
431 <span class="uploaded-item-path">
432 <a href="<%= M.UploadsURL() + filename %>">
433 <%= M.UploadsURL() + filename %>
434 </a>
435 </span>
436 <span class="pull-right">
437 <a href="javascript:void(0);" class="remove" for="<%= filename %>">
438 <span class="glyphicon glyphicon-trash"></span>
439 </a>
440 </span>
441 <span class="clearfix"></span>
407 </div>442 </div>
408 </script>443 </script>
409444