Feature/upload files optional (#196)

* comment.forms pass tmp file to magic

* core.tags settings

* make comment upload image/file optional

* gitignore update

* delete manage

* gitignore update

* delete manage

* generic editor upload

* editor pass image extensions

* fix coffee tests

* bookmark coffee lil refactor

* update js deps

* rename test

* comment.forms change file name format from hash.file.ext -> file_hash.ext

* add @rkenmi to authors

* update history
This commit is contained in:
Esteban Castro Borsani 2017-09-21 13:47:10 -03:00 committed by GitHub
parent ca4b5e047f
commit c2f9ed3a49
47 changed files with 3356 additions and 2360 deletions

10
.gitignore vendored
View File

@ -1,8 +1,10 @@
# Spirit
spirit/search/whoosh_index/
db.sqlite3
example/media/
example/static/
/spirit/search/whoosh_index/
/db.sqlite3
/example/media/
/example/static/
/manage.py
/project
# Node
node_modules

View File

@ -10,3 +10,4 @@ various contributors:
* Robert Kolner @RobertKolner <robert.kolner@gmail.com>
* Benjamin Murden @benmurden <benmurden@github.com>
* Li Dashuang @lidashuang <ldshuang@gmail.com>
* Rick Miyamoto @rkenmi

View File

@ -3,6 +3,11 @@
* Drops support for Python 3.3
* Adds support for Django 1.9 and 1.10
* Adds python-magic dependency (to check uploaded files)
* New: file upload on comments
* Improvement: Adds `ST_UPLOAD_IMAGE_ENABLED`
to enable/disable image uploads and `ST_UPLOAD_FILE_ENABLED`
to enable/disable file uploads
* Remove deprecated `topic_poll` app
* Remove deprecated (since v0.2) `spirit_user.User` (PR #141),
read the wiki or the PR for a workaround

View File

@ -49,7 +49,7 @@ gulp.task('coffee', function() {
pathVendors + '**/*.coffee',
pathCoffee + 'util.coffee',
pathCoffee + 'tab.coffee',
pathCoffee + 'editor_image_upload.coffee',
pathCoffee + 'editor_file_upload.coffee',
pathCoffee + '*.coffee'
])
.pipe(sourcemaps.init())

2002
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,11 +11,17 @@
"gulp-sourcemaps": "^1.6.0",
"gulp-uglify": "^1.4.2",
"gulp-util": "^3.0.6",
"karma": "^0.13.10",
"karma-cli": "^0.1.1",
"karma-jasmine": "^0.2.2"
"jasmine-core": "^2.8.0",
"karma": "^1.7.1",
"karma-cli": "^1.0.1",
"karma-jasmine": "^1.1.0"
},
"scripts": {
"gulp": "gulp"
}
},
"name": "Spirit",
"version": "1.0.0",
"repository": "https://github.com/nitely/Spirit.git",
"author": "nitely <ecastroborsani@gmail.com>",
"license": "MIT"
}

View File

@ -3,14 +3,16 @@
from __future__ import unicode_literals
import os
import logging
import magic
import logging
from django import forms
from django.conf import settings
from django.core.files.storage import default_storage
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_bytes
from django.core.files.uploadedfile import TemporaryUploadedFile
from ..core import utils
from ..core.utils.markdown import Markdown
@ -20,6 +22,7 @@ from .models import Comment
logger = logging.getLogger(__name__)
class CommentForm(forms.ModelForm):
comment = forms.CharField(
max_length=settings.ST_COMMENT_MAX_LEN,
@ -153,35 +156,41 @@ class CommentFileForm(forms.Form):
def clean_file(self):
file = self.cleaned_data['file']
try:
file_mime = magic.from_buffer(file.read(131072), mime=True)
if isinstance(file, TemporaryUploadedFile):
file_mime = magic.from_file(file.temporary_file_path(), mime=True)
else: # In-memory file
file_mime = magic.from_buffer(file.read(), mime=True)
except magic.MagicException as e:
logger.exception(e)
raise forms.ValidationError(_("The file could not be validated"))
else:
# Won't ever raise. Has at most one '.' so lstrip is fine here
ext = os.path.splitext(file.name)[1].lstrip('.')
mime = settings.ST_ALLOWED_UPLOAD_FILE_MEDIA_TYPE.get(ext, None)
if mime is None:
raise forms.ValidationError(
_("Unsupported file extension %s. Supported extensions are %s."
% (ext, ", ".join(settings.ST_ALLOWED_UPLOAD_FILE_MEDIA_TYPE.keys()))
)
)
if mime != file_mime:
raise forms.ValidationError(
_("Unsupported file mime type %s. Supported types are %s."
% (file_mime, ", ".join(settings.ST_ALLOWED_UPLOAD_FILE_MEDIA_TYPE.values()))
)
)
# Won't ever raise. Has at most one '.' so lstrip is fine here
ext = os.path.splitext(file.name)[1].lstrip('.')
mime = settings.ST_ALLOWED_UPLOAD_FILE_MEDIA_TYPE.get(ext, None)
return file
if mime is None:
raise forms.ValidationError(
_("Unsupported file extension %s. Supported extensions are %s." % (
ext,
", ".join(
sorted(settings.ST_ALLOWED_UPLOAD_FILE_MEDIA_TYPE.keys())))))
if mime != file_mime:
raise forms.ValidationError(
_("Unsupported file mime type %s. Supported types are %s." % (
file_mime,
", ".join(
sorted(settings.ST_ALLOWED_UPLOAD_FILE_MEDIA_TYPE.values())))))
return file
def save(self):
file = self.cleaned_data['file']
file_hash = utils.get_file_hash(file)
file.name = ''.join((file_hash, '.', file.name.lower()))
file_name, file_ext = os.path.splitext(file.name.lower())
file.name = ''.join((file_name, '_', file_hash, file_ext))
name = os.path.join('spirit', 'files', str(self.user.pk), file.name)
name = default_storage.save(name, file)
file.url = default_storage.url(name)

View File

@ -2,7 +2,7 @@
from __future__ import unicode_literals
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy as _
from django.utils.html import mark_safe
from django.conf import settings
@ -24,23 +24,29 @@ def render_comments_form(topic, next=None):
@register.simple_tag()
def get_allowed_file_types():
return ".{}".format(", .".join(settings.ST_ALLOWED_UPLOAD_FILE_MEDIA_TYPE.keys()))
return ", ".join(
'.%s' % ext
for ext in sorted(settings.ST_ALLOWED_UPLOAD_FILE_MEDIA_TYPE.keys()))
@register.simple_tag()
def get_allowed_image_types():
return ", ".join(
'.%s' % ext
for ext in sorted(settings.ST_ALLOWED_UPLOAD_IMAGE_FORMAT))
ACTIONS = {
MOVED: _("This topic has been moved"),
CLOSED: _("This topic has been closed"),
UNCLOSED: _("This topic has been unclosed"),
PINNED: _("This topic has been pinned"),
UNPINNED: _("This topic has been unpinned")}
@register.simple_tag()
def get_comment_action_text(action):
if action == MOVED:
return _("This topic has been moved")
elif action == CLOSED:
return _("This topic has been closed")
elif action == UNCLOSED:
return _("This topic has been unclosed")
elif action == PINNED:
return _("This topic has been pinned")
elif action == UNPINNED:
return _("This topic has been unpinned")
else:
return _("Unknown topic moderation action")
return ACTIONS.get(action, _("Unknown topic moderation action"))
@register.simple_tag(takes_context=True)

View File

@ -1,6 +1,8 @@
{% load spirit_tags i18n %}
{% load static from staticfiles %}
{% load_settings 'ST_UPLOAD_IMAGE_ENABLED' 'ST_UPLOAD_FILE_ENABLED' %}
<div class="comment-text js-box-preview-content" style="display:none;"></div>
<ul class="reply-markdown">
<li><a class="js-box-bold" href="#" title="{% trans "Bold" %}"><i class="fa fa-bold"></i></a></li><!--
@ -8,7 +10,9 @@
--><li><a class="js-box-list" href="#" title="{% trans "List" %}"><i class="fa fa-list"></i></a></li><!--
--><li><a class="js-box-url" href="#" title="{% trans "URL" %}"><i class="fa fa-link"></i></a></li><!--
--><li><a class="js-box-image" href="#" title="{% trans "Image" %}"><i class="fa fa-picture-o"></i></a></li><!--
--><li><a class="js-box-file" href="#" title="{% trans "File" %}"><i class="fa fa-file"></i></a></li><!--
-->{% if st_settings.ST_UPLOAD_FILE_ENABLED %}<li>
<a class="js-box-file" href="#" title="{% trans "File" %}"><i class="fa fa-file"></i></a>
</li>{% endif %}<!--
--><li><a class="js-box-poll" href="#" title="{% trans "Poll" %}"><i class="fa fa-bar-chart-o"></i></a></li><!--
--><li><a class="js-box-preview" href="#" title="{% trans "Preview" %}"><i class="fa fa-eye"></i></a></li>
</ul>
@ -28,17 +32,22 @@
} );
$( '.js-reply' ).find( 'textarea' )
.editor_image_upload( {
csrfToken: "{{ csrf_token }}",
target: "{% url "spirit:comment:image-upload-ajax" %}",
placeholderText: "{% trans "uploading {image_name}" %}"
} )
.editor_file_upload({
csrfToken: "{{ csrf_token }}",
target: "{% url "spirit:comment:file-upload-ajax" %}",
placeholderText: "{% trans "uploading {file_name}" %}",
allowedFileMedia: "{% get_allowed_file_types %}"
} )
{% if st_settings.ST_UPLOAD_IMAGE_ENABLED %}
.editor_image_upload( {
csrfToken: "{{ csrf_token }}",
target: "{% url "spirit:comment:image-upload-ajax" %}",
placeholderText: "{% trans "uploading {name}" %}",
allowedFileMedia: "{% get_allowed_image_types %}"
} )
{% endif %}
{% if st_settings.ST_UPLOAD_FILE_ENABLED %}
.editor_file_upload({
csrfToken: "{{ csrf_token }}",
target: "{% url "spirit:comment:file-upload-ajax" %}",
placeholderText: "{% trans "uploading {name}" %}",
allowedFileMedia: "{% get_allowed_file_types %}"
} )
{% endif %}
.editor( {
boldedText: "{% trans "bolded text" %}",
italicisedText: "{% trans "italicised text" %}",

View File

@ -456,30 +456,70 @@ class CommentViewTest(TestCase):
self.assertIn('error', res.keys())
self.assertIn('image', res['error'].keys())
@override_settings(MEDIA_ROOT=os.path.join(settings.BASE_DIR, 'media_test'))
@override_settings(
MEDIA_ROOT=os.path.join(settings.BASE_DIR, 'media_test'),
FILE_UPLOAD_MAX_MEMORY_SIZE=2621440)
def test_comment_file_upload(self):
"""
comment file upload
Check (in-memory) upload files are checked
"""
utils.login(self)
# sample valid pdf - https://stackoverflow.com/a/17280876
file = BytesIO(b'%PDF-1.0\n1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1'
b'>>endobj 3 0 obj<</Type/Page/MediaBox[0 0 3 3]>>endobj\nxref\n0 4\n0000000000 65535 f\n000000'
b'0010 00000 n\n0000000053 00000 n\n0000000102 00000 n\ntrailer<</Size 4/Root 1 0 R>>\nstartxre'
b'f\n149\n%EOF\n')
file = BytesIO(
b'%PDF-1.0\n1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1'
b'>>endobj 3 0 obj<</Type/Page/MediaBox[0 0 3 3]>>endobj\nxref\n0 4\n0000000000 65535 f\n000000'
b'0010 00000 n\n0000000053 00000 n\n0000000102 00000 n\ntrailer<</Size 4/Root 1 0 R>>\nstartxre'
b'f\n149\n%EOF\n')
files = {'file': SimpleUploadedFile('file.pdf', file.read(), content_type='application/pdf'), }
response = self.client.post(reverse('spirit:comment:file-upload-ajax'),
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
data=files)
response = self.client.post(
reverse('spirit:comment:file-upload-ajax'),
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
data=files)
res = json.loads(response.content.decode('utf-8'))
file_url = os.path.join(
settings.MEDIA_URL, 'spirit', 'files', str(self.user.pk), "fadcb2389bb2b69b46bc54185de0ae91.file.pdf"
settings.MEDIA_URL, 'spirit', 'files', str(self.user.pk), "file_fadcb2389bb2b69b46bc54185de0ae91.pdf"
).replace("\\", "/")
self.assertEqual(res['url'], file_url)
file_path = os.path.join(
settings.MEDIA_ROOT, 'spirit', 'files', str(self.user.pk), "fadcb2389bb2b69b46bc54185de0ae91.file.pdf"
settings.MEDIA_ROOT, 'spirit', 'files', str(self.user.pk), "file_fadcb2389bb2b69b46bc54185de0ae91.pdf"
)
with open(file_path, 'rb') as fh:
file.seek(0)
self.assertEqual(fh.read(), file.read())
shutil.rmtree(settings.MEDIA_ROOT) # cleanup
@override_settings(
MEDIA_ROOT=os.path.join(settings.BASE_DIR, 'media_test'),
FILE_UPLOAD_MAX_MEMORY_SIZE=1)
def test_comment_file_upload_tmp_file(self):
"""
Check (tmp) upload files are checked
"""
utils.login(self)
file = BytesIO(
b'%PDF-1.0\n1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1'
b'>>endobj 3 0 obj<</Type/Page/MediaBox[0 0 3 3]>>endobj\nxref\n0 4\n0000000000 65535 f\n000000'
b'0010 00000 n\n0000000053 00000 n\n0000000102 00000 n\ntrailer<</Size 4/Root 1 0 R>>\nstartxre'
b'f\n149\n%EOF\n')
files = {
'file': SimpleUploadedFile(
'file_large.pdf', file.read(), content_type='application/pdf'),}
response = self.client.post(
reverse('spirit:comment:file-upload-ajax'),
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
data=files)
res = json.loads(response.content.decode('utf-8'))
file_url = os.path.join(
settings.MEDIA_URL, 'spirit', 'files', str(self.user.pk), "file_large_fadcb2389bb2b69b46bc54185de0ae91.pdf"
).replace("\\", "/")
self.assertEqual(res['url'], file_url)
file_path = os.path.join(
settings.MEDIA_ROOT, 'spirit', 'files', str(self.user.pk), "file_large_fadcb2389bb2b69b46bc54185de0ae91.pdf"
)
with open(file_path, 'rb') as fh:
@ -494,19 +534,22 @@ class CommentViewTest(TestCase):
"""
utils.login(self)
# sample valid pdf - https://stackoverflow.com/a/17280876
file = BytesIO(b'%PDF-1.0\n1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1'
b'>>endobj 3 0 obj<</Type/Page/MediaBox[0 0 3 3]>>endobj\nxref\n0 4\n0000000000 65535 f\n000000'
b'0010 00000 n\n0000000053 00000 n\n0000000102 00000 n\ntrailer<</Size 4/Root 1 0 R>>\nstartxre'
b'f\n149\n%EOF\n')
file = BytesIO(
b'%PDF-1.0\n1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1'
b'>>endobj 3 0 obj<</Type/Page/MediaBox[0 0 3 3]>>endobj\nxref\n0 4\n0000000000 65535 f\n000000'
b'0010 00000 n\n0000000053 00000 n\n0000000102 00000 n\ntrailer<</Size 4/Root 1 0 R>>\nstartxre'
b'f\n149\n%EOF\n')
files = {'file': SimpleUploadedFile('fake.gif', file.read(), content_type='application/pdf'), }
response = self.client.post(reverse('spirit:comment:file-upload-ajax'),
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
data=files)
response = self.client.post(
reverse('spirit:comment:file-upload-ajax'),
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
data=files)
res = json.loads(response.content.decode('utf-8'))
self.assertIn('error', res.keys())
self.assertIn('file', res['error'].keys())
self.assertEqual(res['error']['file'],
['Unsupported file extension gif. Supported extensions are doc, docx, pdf.'])
self.assertIn('error', res)
self.assertIn('file', res['error'])
self.assertEqual(
res['error']['file'],
['Unsupported file extension gif. Supported extensions are doc, docx, pdf.'])
def test_comment_file_upload_invalid_mime(self):
"""
@ -514,19 +557,22 @@ class CommentViewTest(TestCase):
"""
utils.login(self)
file = BytesIO(b'BAD\x02D\x01\x00;')
files = {'file': SimpleUploadedFile('file.pdf', file.read(), content_type='application/pdf'), }
response = self.client.post(reverse('spirit:comment:file-upload-ajax'),
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
data=files)
files = {
'file': SimpleUploadedFile(
'file.pdf', file.read(), content_type='application/pdf')}
response = self.client.post(
reverse('spirit:comment:file-upload-ajax'),
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
data=files)
res = json.loads(response.content.decode('utf-8'))
self.assertIn('error', res.keys())
self.assertIn('file', res['error'].keys())
self.assertEqual(res['error']['file'],
[
'Unsupported file mime type application/octet-stream. '
'Supported types are application/msword, '
'application/vnd.openxmlformats-officedocument.wordprocessingml.document, '
'application/pdf.'])
self.assertIn('error', res)
self.assertIn('file', res['error'])
self.assertEqual(
res['error']['file'],
['Unsupported file mime type application/octet-stream. '
'Supported types are application/msword, '
'application/pdf, '
'application/vnd.openxmlformats-officedocument.wordprocessingml.document.'])
class CommentModelsTest(TestCase):

View File

@ -3,6 +3,7 @@
from __future__ import unicode_literals
from django.conf.urls import url, include
from django.conf import settings
import spirit.comment.bookmark.urls
import spirit.comment.flag.urls
@ -23,12 +24,17 @@ urlpatterns = [
url(r'^(?P<pk>\d+)/delete/$', views.delete, name='delete'),
url(r'^(?P<pk>\d+)/undelete/$', views.delete, kwargs={'remove': False, }, name='undelete'),
url(r'^upload/$', views.image_upload_ajax, name='image-upload-ajax'),
url(r'^upload/file/$', views.file_upload_ajax, name='file-upload-ajax'),
url(r'^bookmark/', include(spirit.comment.bookmark.urls, namespace='bookmark')),
url(r'^flag/', include(spirit.comment.flag.urls, namespace='flag')),
url(r'^history/', include(spirit.comment.history.urls, namespace='history')),
url(r'^like/', include(spirit.comment.like.urls, namespace='like')),
url(r'^poll/', include(spirit.comment.poll.urls, namespace='poll')),
]
if settings.ST_UPLOAD_IMAGE_ENABLED:
urlpatterns.append(
url(r'^upload/$', views.image_upload_ajax, name='image-upload-ajax'))
if settings.ST_UPLOAD_FILE_ENABLED:
urlpatterns.append(
url(r'^upload/file/$', views.file_upload_ajax, name='file-upload-ajax'))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -63,17 +63,16 @@
};
Bookmark.prototype.sendCommentNumber = function() {
var post, sentCommentNumber;
var sentCommentNumber;
if (this.mark.isSending) {
return;
}
this.mark.isSending = true;
sentCommentNumber = this.mark.commentNumber;
post = $.post(this.options.target, {
return $.post(this.options.target, {
csrfmiddlewaretoken: this.options.csrfToken,
comment_number: this.mark.commentNumber
});
return post.always((function(_this) {
}).always((function(_this) {
return function() {
_this.mark.isSending = false;
if (_this.mark.commentNumber > sentCommentNumber) {

View File

@ -5,150 +5,176 @@
*/
(function() {
var $, EditorImageUpload,
var $, EditorUpload,
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
$ = jQuery;
EditorImageUpload = (function() {
EditorImageUpload.prototype.defaults = {
EditorUpload = (function() {
EditorUpload.prototype.defaults = {
csrfToken: "csrf_token",
target: "target url",
placeholderText: "uploading {image_name}"
placeholderText: "uploading {name}",
allowedFileMedia: ["*/*"]
};
function EditorImageUpload(el, options) {
EditorUpload.prototype._meta = {
fieldName: "file",
tag: "[{text}]({url})",
elm: ".js-box-file"
};
function EditorUpload(el, options, meta) {
if (meta == null) {
meta = null;
}
this.openFileDialog = bind(this.openFileDialog, this);
this.textReplace = bind(this.textReplace, this);
this.addStatusError = bind(this.addStatusError, this);
this.addError = bind(this.addError, this);
this.addImage = bind(this.addImage, this);
this.addFile = bind(this.addFile, this);
this.buildFormData = bind(this.buildFormData, this);
this.addPlaceholder = bind(this.addPlaceholder, this);
this.sendFile = bind(this.sendFile, this);
this.el = $(el);
this.options = $.extend({}, this.defaults, options);
this.meta = $.extend({}, this._meta, meta || {});
this.formFile = $("<form/>");
this.inputFile = $("<input/>", {
type: "file",
accept: "image/*"
accept: this.options.allowedFileMedia
}).appendTo(this.formFile);
this.setUp();
}
EditorImageUpload.prototype.setUp = function() {
var $boxImage;
EditorUpload.prototype.setUp = function() {
var $boxElm;
if (window.FormData == null) {
return;
}
this.inputFile.on('change', this.sendFile);
$boxImage = $(".js-box-image");
$boxImage.on('click', this.openFileDialog);
return $boxImage.on('click', this.stopClick);
$boxElm = $(this.meta.elm);
$boxElm.on('click', this.openFileDialog);
return $boxElm.on('click', this.stopClick);
};
EditorImageUpload.prototype.sendFile = function() {
var file, formData, placeholder, post;
EditorUpload.prototype.sendFile = function() {
var file, formData, placeholder;
file = this.inputFile.get(0).files[0];
placeholder = this.addPlaceholder(file);
formData = this.buildFormData(file);
post = $.ajax({
$.ajax({
url: this.options.target,
data: formData,
processData: false,
contentType: false,
type: 'POST'
});
post.done((function(_this) {
}).done((function(_this) {
return function(data) {
if ("url" in data) {
return _this.addImage(data, file, placeholder);
return _this.addFile(data, file, placeholder);
} else {
return _this.addError(data, placeholder);
}
};
})(this));
post.fail((function(_this) {
})(this)).fail((function(_this) {
return function(jqxhr, textStatus, error) {
return _this.addStatusError(textStatus, error, placeholder);
};
})(this));
post.always((function(_this) {
})(this)).always((function(_this) {
return function() {
return _this.formFile.get(0).reset();
};
})(this));
};
EditorImageUpload.prototype.addPlaceholder = function(file) {
EditorUpload.prototype.addPlaceholder = function(file) {
var placeholder;
placeholder = $.format("![" + this.options.placeholderText + "]()", {
image_name: file.name
placeholder = $.format(this.meta.tag, {
text: $.format(this.options.placeholderText, {
name: file.name
}),
url: ""
});
this.el.val(this.el.val() + placeholder);
return placeholder;
};
EditorImageUpload.prototype.buildFormData = function(file) {
EditorUpload.prototype.buildFormData = function(file) {
var formData;
formData = new FormData();
formData.append('csrfmiddlewaretoken', this.options.csrfToken);
formData.append('image', file);
formData.append(this.meta.fieldName, file);
return formData;
};
EditorImageUpload.prototype.addImage = function(data, file, placeholder) {
EditorUpload.prototype.addFile = function(data, file, placeholder) {
var imageTag;
imageTag = $.format("![{name}]({url})", {
name: file.name,
imageTag = $.format(this.meta.tag, {
text: file.name,
url: data.url
});
return this.textReplace(placeholder, imageTag);
};
EditorImageUpload.prototype.addError = function(data, placeholder) {
EditorUpload.prototype.addError = function(data, placeholder) {
var error;
error = JSON.stringify(data);
return this.textReplace(placeholder, "![" + error + "]()");
return this.textReplace(placeholder, $.format(this.meta.tag, {
text: error,
url: ""
}));
};
EditorImageUpload.prototype.addStatusError = function(textStatus, error, placeholder) {
EditorUpload.prototype.addStatusError = function(textStatus, error, placeholder) {
var errorTag;
errorTag = $.format("![error: {code} {error}]()", {
code: textStatus,
error: error
errorTag = $.format(this.meta.tag, {
text: $.format("error: {code} {error}", {
code: textStatus,
error: error
}),
url: ""
});
return this.textReplace(placeholder, errorTag);
};
EditorImageUpload.prototype.textReplace = function(find, replace) {
EditorUpload.prototype.textReplace = function(find, replace) {
this.el.val(this.el.val().replace(find, replace));
};
EditorImageUpload.prototype.openFileDialog = function() {
EditorUpload.prototype.openFileDialog = function() {
this.inputFile.trigger('click');
};
EditorImageUpload.prototype.stopClick = function(e) {
EditorUpload.prototype.stopClick = function(e) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
};
return EditorImageUpload;
return EditorUpload;
})();
$.fn.extend({
editor_file_upload: function(options) {
return this.each(function() {
if (!$(this).data('plugin_editor_file_upload')) {
return $(this).data('plugin_editor_file_upload', new EditorUpload(this, options));
}
});
},
editor_image_upload: function(options) {
return this.each(function() {
if (!$(this).data('plugin_editor_image_upload')) {
return $(this).data('plugin_editor_image_upload', new EditorImageUpload(this, options));
return $(this).data('plugin_editor_image_upload', new EditorUpload(this, options, {
fieldName: "image",
tag: "![{text}]({url})",
elm: ".js-box-image"
}));
}
});
}
});
$.fn.editor_image_upload.EditorImageUpload = EditorImageUpload;
}).call(this);

View File

@ -35,7 +35,7 @@
};
Storage.prototype.updateStorage = function() {
var err, error, value;
var err, value;
value = this.el.val();
try {
localStorage[this.lsKey] = value;

View File

@ -1,15 +1,29 @@
## Installation (Ubuntu 14.04)
## Installation (Ubuntu 16.04)
### Install Node.js
Install io.js
```
$ curl -sL https://deb.nodesource.com/setup_iojs_3.x | sudo -E bash -
$ sudo apt-get install -y iojs
$ curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
$ sudo apt-get install -y nodejs
```
Install dependencies
> If you are a node.js user then install nvm to manage the versions
### Install yarn (js package manager)
```
$ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
$ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
$ sudo apt-get update && sudo apt-get install yarn
```
> On Ubuntu 17 you may want to remove `cmdtest` if you get any error `sudo apt remove cmdtest`
### Install dependencies
```
$ cd ./Spirit
$ npm install -y .
$ yarn
```
## Build

View File

@ -57,15 +57,14 @@ class Bookmark
@mark.isSending = true
sentCommentNumber = @mark.commentNumber
post = $.post(
$.post(
@options.target,
{
csrfmiddlewaretoken: @options.csrfToken,
comment_number: @mark.commentNumber
}
)
post.always( =>
.always( =>
@mark.isSending = false
if @mark.commentNumber > sentCommentNumber

View File

@ -16,8 +16,6 @@ class Editor
linkUrlText: "link url",
imageText: "image text",
imageUrlText: "image url",
fileText: "file text",
fileUrlText: "file url",
pollTitleText: "Title",
pollChoiceText: "Description"
}
@ -37,7 +35,6 @@ class Editor
$('.js-box-list').on('click', @addList)
$('.js-box-url').on('click', @addUrl)
$('.js-box-image').on('click', @addImage)
$('.js-box-file').on('click', @addFile)
$('.js-box-poll').on('click', @addPoll)
$('.js-box-preview').on('click', @togglePreview)
@ -77,10 +74,6 @@ class Editor
@wrapSelection("![", "](#{ @options.imageUrlText })", @options.imageText)
return false
addFile: =>
@wrapSelection("[", "](#{ @options.fileUrlText })", @options.fileText)
return false
addPoll: =>
poll = "\n\n[poll name=#{@pollCounter}]\n" +
"# #{@options.pollTitleText}\n" +

View File

@ -6,18 +6,24 @@
$ = jQuery
class EditorFileUpload
class EditorUpload
defaults: {
csrfToken: "csrf_token",
target: "target url",
placeholderText: "uploading {file_name}",
allowedFileMedia: [".doc", ".docx", ".pdf"]
placeholderText: "uploading {name}",
allowedFileMedia: ["*/*"]
}
_meta: {
fieldName: "file",
tag: "[{text}]({url})",
elm: ".js-box-file"
}
constructor: (el, options) ->
@el = $(el)
constructor: (el, options, meta=null) ->
@el = $(el) # Editor box
@options = $.extend({}, @defaults, options)
@meta = $.extend({}, @_meta, meta or {})
@formFile = $("<form/>")
@inputFile = $("<input/>", {
type: "file",
@ -33,35 +39,32 @@ class EditorFileUpload
# TODO: fixme, having multiple editors
# in the same page would open several
# dialogs on box-image click
$boxImage = $(".js-box-file")
$boxImage.on('click', @openFileDialog)
$boxImage.on('click', @stopClick)
$boxElm = $(@meta.elm)
$boxElm.on('click', @openFileDialog)
$boxElm.on('click', @stopClick)
sendFile: =>
file = @inputFile.get(0).files[0]
placeholder = @addPlaceholder(file)
formData = @buildFormData(file)
post = $.ajax({
$.ajax({
url: @options.target,
data: formData,
processData: false,
contentType: false,
type: 'POST'
})
post.done((data) =>
.done((data) =>
if "url" of data
@addFile(data, file, placeholder)
else
@addError(data, placeholder)
)
post.fail((jqxhr, textStatus, error) =>
.fail((jqxhr, textStatus, error) =>
@addStatusError(textStatus, error, placeholder)
)
post.always(() =>
.always(() =>
# Reset the input after uploading,
# fixes uploading the same image twice
@formFile.get(0).reset()
@ -70,27 +73,34 @@ class EditorFileUpload
return
addPlaceholder: (file) =>
placeholder = $.format("[#{ @options.placeholderText }]()", {file_name: file.name, })
placeholder = $.format(@meta.tag, {
text: $.format(@options.placeholderText, {name: file.name}),
url: ""})
@el.val(@el.val() + placeholder)
return placeholder
buildFormData: (file) =>
formData = new FormData()
formData.append('csrfmiddlewaretoken', @options.csrfToken)
formData.append('file', file)
formData.append(@meta.fieldName, file)
return formData
addFile: (data, file, placeholder) =>
# format as a link to the file
fileTag = $.format("[{name}]({url})", {name: file.name, url: data.url})
@textReplace(placeholder, fileTag)
imageTag = $.format(@meta.tag, {text: file.name, url: data.url})
@textReplace(placeholder, imageTag)
addError: (data, placeholder) =>
error = JSON.stringify(data)
@textReplace(placeholder, "[#{ error }]()")
@textReplace(
placeholder,
$.format(@meta.tag, {text: error, url: ""}))
addStatusError: (textStatus, error, placeholder) =>
errorTag = $.format("[error: {code} {error}]()", {code: textStatus, error: error})
errorTag = $.format(@meta.tag, {
text: $.format("error: {code} {error}", {
code: textStatus,
error: error}),
url: ""})
@textReplace(placeholder, errorTag)
textReplace: (find, replace) =>
@ -112,7 +122,14 @@ $.fn.extend
editor_file_upload: (options) ->
@each( ->
if not $(@).data('plugin_editor_file_upload')
$(@).data('plugin_editor_file_upload', new EditorFileUpload(@, options))
$(@).data('plugin_editor_file_upload', new EditorUpload(@, options))
)
editor_image_upload: (options) ->
@each( ->
if not $(@).data('plugin_editor_image_upload')
$(@).data('plugin_editor_image_upload', new EditorUpload(@, options, {
fieldName: "image",
tag: "![{text}]({url})",
elm: ".js-box-image"
}))
)
$.fn.editor_file_upload.EditorFileUpload = EditorFileUpload

View File

@ -1,116 +0,0 @@
###
Markdown editor image upload, should be loaded before $.editor()
requires: util.js
###
$ = jQuery
class EditorImageUpload
defaults: {
csrfToken: "csrf_token",
target: "target url",
placeholderText: "uploading {image_name}"
}
constructor: (el, options) ->
@el = $(el)
@options = $.extend({}, @defaults, options)
@formFile = $("<form/>")
@inputFile = $("<input/>", {
type: "file",
accept: "image/*"}).appendTo(@formFile)
@setUp()
setUp: ->
if not window.FormData?
return
@inputFile.on('change', @sendFile)
# TODO: fixme, having multiple editors
# in the same page would open several
# dialogs on box-image click
$boxImage = $(".js-box-image")
$boxImage.on('click', @openFileDialog)
$boxImage.on('click', @stopClick)
sendFile: =>
file = @inputFile.get(0).files[0]
placeholder = @addPlaceholder(file)
formData = @buildFormData(file)
post = $.ajax({
url: @options.target,
data: formData,
processData: false,
contentType: false,
type: 'POST'
})
post.done((data) =>
if "url" of data
@addImage(data, file, placeholder)
else
@addError(data, placeholder)
)
post.fail((jqxhr, textStatus, error) =>
@addStatusError(textStatus, error, placeholder)
)
post.always(() =>
# Reset the input after uploading,
# fixes uploading the same image twice
@formFile.get(0).reset()
)
return
addPlaceholder: (file) =>
placeholder = $.format("![#{ @options.placeholderText }]()", {image_name: file.name, })
@el.val(@el.val() + placeholder)
return placeholder
buildFormData: (file) =>
formData = new FormData()
formData.append('csrfmiddlewaretoken', @options.csrfToken)
formData.append('image', file)
return formData
addImage: (data, file, placeholder) =>
imageTag = $.format("![{name}]({url})", {name: file.name, url: data.url})
@textReplace(placeholder, imageTag)
addError: (data, placeholder) =>
error = JSON.stringify(data)
@textReplace(placeholder, "![#{ error }]()")
addStatusError: (textStatus, error, placeholder) =>
errorTag = $.format("![error: {code} {error}]()", {code: textStatus, error: error})
@textReplace(placeholder, errorTag)
textReplace: (find, replace) =>
@el.val(@el.val().replace(find, replace))
return
openFileDialog: =>
@inputFile.trigger('click')
return
stopClick: (e) ->
e.preventDefault()
e.stopPropagation()
e.stopImmediatePropagation()
return
$.fn.extend
editor_image_upload: (options) ->
@each( ->
if not $(@).data('plugin_editor_image_upload')
$(@).data('plugin_editor_image_upload', new EditorImageUpload(@, options))
)
$.fn.editor_image_upload.EditorImageUpload = EditorImageUpload

File diff suppressed because one or more lines are too long

View File

@ -14,9 +14,7 @@ describe "editor plugin tests", ->
linkText: "foo link text",
linkUrlText: "foo link url",
imageText: "foo image text",
imageUrlText: "foo image url",
fileText: "foo file text",
fileUrlText: "foo file url"
imageUrlText: "foo image url"
}
editor = textarea.data 'plugin_editor'
@ -53,10 +51,6 @@ describe "editor plugin tests", ->
$('.js-box-image').trigger 'click'
expect(textarea.val()).toEqual "![foo image text](foo image url)"
it "adds file", ->
$('.js-box-file').trigger 'click'
expect(textarea.val()).toEqual "[foo file text](foo file url)"
it "adds all", ->
$('.js-box-bold').trigger 'click'
$('.js-box-italic').trigger 'click'
@ -106,14 +100,6 @@ describe "editor plugin tests", ->
$('.js-box-image').trigger 'click'
expect(textarea.val()).toEqual "bir![foo](foo image url)bar"
it "wraps the selected text, file", ->
textarea.val "birfoobar"
textarea.first()[0].selectionStart = 3
textarea.first()[0].selectionEnd = 6
$('.js-box-file').trigger 'click'
expect(textarea.val()).toEqual "bir[foo](foo file url)bar"
it "shows html preview", ->
textarea.val "*foo*"
$('.js-box-preview').trigger 'click'

View File

@ -56,7 +56,8 @@
$('.js-box-italic').trigger('click');
$('.js-box-list').trigger('click');
$('.js-box-url').trigger('click');
return $('.js-box-image').trigger('click');
$('.js-box-image').trigger('click');
return $('.js-box-file').trigger('click');
});
it("wraps the selected text, bold", function() {
textarea.val("birfoobar");

File diff suppressed because one or more lines are too long

View File

@ -26,7 +26,8 @@ describe "editor file upload plugin tests", ->
textarea = $('#id_comment').editor_file_upload {
csrfToken: "foo csrf_token",
target: "/foo/",
placeholderText: "foo uploading {file_name}"
placeholderText: "foo uploading {name}",
allowedFileMedia: ".doc,.docx,.pdf"
}
editorFileUpload = textarea.first().data 'plugin_editor_file_upload'
inputFile = editorFileUpload.inputFile

View File

@ -0,0 +1,141 @@
(function() {
describe("editor file upload plugin tests", function() {
var data, editorFileUpload, file, inputFile, post, textarea;
textarea = null;
editorFileUpload = null;
data = null;
inputFile = null;
file = null;
post = null;
beforeEach(function() {
var fixtures;
fixtures = jasmine.getFixtures();
fixtures.fixturesPath = 'base/test/fixtures/';
loadFixtures('editor.html');
post = spyOn($, 'ajax');
post.and.callFake(function(req) {
var d;
d = $.Deferred();
d.resolve(data);
return d.promise();
});
data = {
url: "/path/file.pdf"
};
file = {
name: "foo.pdf"
};
textarea = $('#id_comment').editor_file_upload({
csrfToken: "foo csrf_token",
target: "/foo/",
placeholderText: "foo uploading {name}",
allowedFileMedia: ".doc,.docx,.pdf"
});
editorFileUpload = textarea.first().data('plugin_editor_file_upload');
return inputFile = editorFileUpload.inputFile;
});
it("doesnt break selector chaining", function() {
expect(textarea).toEqual($('#id_comment'));
return expect(textarea.length).toEqual(1);
});
it("does nothing if the browser is not supported", function() {
var inputFile2, org_formData, textarea2, trigger;
org_formData = window.FormData;
window.FormData = null;
try {
$(".js-box-file").off('click');
textarea2 = $('#id_comment2').editor_file_upload();
inputFile2 = textarea2.data('plugin_editor_file_upload').inputFile;
trigger = spyOn(inputFile2, 'trigger');
$(".js-box-file").trigger('click');
return expect(trigger).not.toHaveBeenCalled();
} finally {
window.FormData = org_formData;
}
});
it("opens the file choose dialog", function() {
var trigger;
trigger = spyOn(inputFile, 'trigger');
$(".js-box-file").trigger('click');
return expect(trigger).toHaveBeenCalled();
});
it("uploads the file", function() {
var formDataMock;
expect($.ajax.calls.any()).toEqual(false);
formDataMock = jasmine.createSpyObj('formDataMock', ['append']);
spyOn(window, "FormData").and.returnValue(formDataMock);
spyOn(inputFile, 'get').and.returnValue({
files: [file]
});
inputFile.trigger('change');
expect($.ajax.calls.any()).toEqual(true);
expect($.ajax.calls.argsFor(0)).toEqual([
{
url: '/foo/',
data: formDataMock,
processData: false,
contentType: false,
type: 'POST'
}
]);
expect(formDataMock.append).toHaveBeenCalledWith('csrfmiddlewaretoken', 'foo csrf_token');
return expect(formDataMock.append).toHaveBeenCalledWith('file', {
name: 'foo.pdf'
});
});
it("changes the placeholder on upload success", function() {
textarea.val("foobar");
spyOn(inputFile, 'get').and.returnValue({
files: [file]
});
inputFile.trigger('change');
return expect(textarea.val()).toEqual("foobar[foo.pdf](/path/file.pdf)");
});
it("changes the placeholder on upload error", function() {
textarea.val("foobar");
data = {
error: {
foo: "foo error"
}
};
spyOn(inputFile, 'get').and.returnValue({
files: [file]
});
inputFile.trigger('change');
return expect(textarea.val()).toEqual("foobar[{\"error\":{\"foo\":\"foo error\"}}]()");
});
it("changes the placeholder on upload failure", function() {
var d;
textarea.val("foobar");
d = $.Deferred();
post.and.callFake(function(req) {
d.reject(null, "foo statusError", "bar error");
return d.promise();
});
spyOn(inputFile, 'get').and.returnValue({
files: [file]
});
inputFile.trigger('change');
return expect(textarea.val()).toEqual("foobar[error: foo statusError bar error]()");
});
it("checks for default media file extensions if none are provided", function() {
return expect(inputFile[0].outerHTML).toContain(".doc,.docx,.pdf");
});
return it("checks for custom media file extensions if they are provided", function() {
var editorFileUpload3, inputFile3, textarea3;
textarea3 = $('#id_comment3').editor_file_upload({
csrfToken: "foo csrf_token",
target: "/foo/",
placeholderText: "foo uploading {file_name}",
allowedFileMedia: [".superdoc"]
});
editorFileUpload3 = textarea3.first().data('plugin_editor_file_upload');
inputFile3 = editorFileUpload3.inputFile;
expect(inputFile3[0].outerHTML).not.toContain(".doc,.docx,.pdf");
return expect(inputFile3[0].outerHTML).toContain(".superdoc");
});
});
}).call(this);
//# sourceMappingURL=editor_file_upload-spec.js.map

File diff suppressed because one or more lines are too long

View File

@ -26,7 +26,7 @@ describe "editor image upload plugin tests", ->
textarea = $('#id_comment').editor_image_upload {
csrfToken: "foo csrf_token",
target: "/foo/",
placeholderText: "foo uploading {image_name}"
placeholderText: "foo uploading {name}"
}
editorImageUpload = textarea.first().data 'plugin_editor_image_upload'
inputFile = editorImageUpload.inputFile
@ -71,12 +71,15 @@ describe "editor image upload plugin tests", ->
it "adds the placeholder", ->
textarea.val "foobar"
ajaxMock = jasmine.createSpyObj('ajax', ['done', 'fail'])
post.and.returnValue ajaxMock
post.and.callFake (req) ->
expect(textarea.val()).toEqual "foobar![foo uploading foo.jpg]()"
d = $.Deferred()
d.resolve(data)
return d.promise()
spyOn(inputFile, 'get').and.returnValue {files: [file, ]}
inputFile.trigger 'change'
expect(textarea.val()).toEqual "foobar![foo uploading foo.jpg]()"
it "changes the placeholder on upload success", ->
textarea.val "foobar"

View File

@ -28,7 +28,7 @@
textarea = $('#id_comment').editor_image_upload({
csrfToken: "foo csrf_token",
target: "/foo/",
placeholderText: "foo uploading {image_name}"
placeholderText: "foo uploading {name}"
});
editorImageUpload = textarea.first().data('plugin_editor_image_upload');
return inputFile = editorImageUpload.inputFile;
@ -83,15 +83,18 @@
});
});
it("adds the placeholder", function() {
var ajaxMock;
textarea.val("foobar");
ajaxMock = jasmine.createSpyObj('ajax', ['done', 'fail']);
post.and.returnValue(ajaxMock);
post.and.callFake(function(req) {
var d;
expect(textarea.val()).toEqual("foobar![foo uploading foo.jpg]()");
d = $.Deferred();
d.resolve(data);
return d.promise();
});
spyOn(inputFile, 'get').and.returnValue({
files: [file]
});
inputFile.trigger('change');
return expect(textarea.val()).toEqual("foobar![foo uploading foo.jpg]()");
return inputFile.trigger('change');
});
it("changes the placeholder on upload success", function() {
textarea.val("foobar");

File diff suppressed because one or more lines are too long

View File

@ -2,4 +2,4 @@ describe "editor plugin tests", ->
it "returns the emoji list", ->
emojis = $.emoji_list()
expect(emojis[0]).toEqual "+1"
expect(emojis[0]).toEqual {"name": "+1"}

View File

@ -3,7 +3,9 @@
return it("returns the emoji list", function() {
var emojis;
emojis = $.emoji_list();
return expect(emojis[0]).toEqual("+1");
return expect(emojis[0]).toEqual({
"name": "+1"
});
});
});

View File

@ -1 +1 @@
{"version":3,"sources":["emoji_list-spec.coffee"],"names":[],"mappings":"AAAA;EAAA,QAAA,CAAS,qBAAT,EAAgC,SAAA;WAE5B,EAAA,CAAG,wBAAH,EAA6B,SAAA;AACzB,UAAA;MAAA,MAAA,GAAS,CAAC,CAAC,UAAF,CAAA;aACT,MAAA,CAAO,MAAO,CAAA,CAAA,CAAd,CAAiB,CAAC,OAAlB,CAA0B,IAA1B;IAFyB,CAA7B;EAF4B,CAAhC;AAAA","file":"emoji_list-spec.js","sourceRoot":"/source/","sourcesContent":["describe \"editor plugin tests\", ->\n\n it \"returns the emoji list\", ->\n emojis = $.emoji_list()\n expect(emojis[0]).toEqual \"+1\"\n"]}
{"version":3,"file":"emoji_list-spec.js","sources":["emoji_list-spec.coffee"],"names":[],"mappings":"AAAA;EAAA,QAAA,CAAS,qBAAT,EAAgC,SAAA;WAE5B,EAAA,CAAG,wBAAH,EAA6B,SAAA;AACzB,UAAA;MAAA,MAAA,GAAS,CAAC,CAAC,UAAF,CAAA;aACT,MAAA,CAAO,MAAO,CAAA,CAAA,CAAd,CAAiB,CAAC,OAAlB,CAA0B;QAAC,MAAA,EAAQ,IAAT;OAA1B;IAFyB,CAA7B;EAF4B,CAAhC;AAAA","sourcesContent":["describe \"editor plugin tests\", ->\n\n it \"returns the emoji list\", ->\n emojis = $.emoji_list()\n expect(emojis[0]).toEqual {\"name\": \"+1\"}\n"]}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
{"version":3,"sources":["postify-spec.coffee"],"names":[],"mappings":"AAAA;EAAA,QAAA,CAAS,sBAAT,EAAiC,SAAA;AAC7B,QAAA;IAAA,MAAA,GAAS;IACT,cAAA,GAAiB;IACjB,OAAA,GAAU;IAEV,UAAA,CAAW,SAAA;AACP,UAAA;MAAA,QAAA,GAAc,OAAO,CAAC,WAAX,CAAA;MACX,QAAQ,CAAC,YAAT,GAAwB;MACxB,YAAA,CAAa,cAAb;MAEA,MAAA,GAAS,CAAA,CAAE,WAAF,CAAc,CAAC,OAAf,CAAuB;QAAC,SAAA,EAAW,QAAZ;OAAvB;MACT,cAAA,GAAiB,MAAM,CAAC,KAAP,CAAA,CAAc,CAAC,IAAf,CAAoB,gBAApB;aACjB,OAAA,GAAU,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC;IAPhB,CAAX;IASA,EAAA,CAAG,gCAAH,EAAqC,SAAA;MACjC,MAAA,CAAO,MAAP,CAAc,CAAC,OAAf,CAAuB,CAAA,CAAE,UAAF,CAAvB;aACA,MAAA,CAAO,MAAM,CAAC,MAAd,CAAqB,CAAC,OAAtB,CAA8B,CAA9B;IAFiC,CAArC;IAIA,EAAA,CAAG,+CAAH,EAAoD,SAAA;AAChD,UAAA;MAAA,KAAA,GAAQ;QAAC,IAAA,EAAM,OAAP;QAAgB,eAAA,EAAiB,CAAC,SAAA,GAAA,CAAD,CAAjC;QAAuC,cAAA,EAAgB,CAAC,SAAA,GAAA,CAAD,CAAvD;;MACR,eAAA,GAAkB,KAAA,CAAM,KAAN,EAAa,iBAAb;MAClB,cAAA,GAAiB,KAAA,CAAM,KAAN,EAAa,gBAAb;MAEjB,KAAA,CAAM,cAAN,EAAsB,YAAtB;MACA,CAAA,CAAE,UAAF,CAAa,CAAC,KAAd,CAAA,CAAqB,CAAC,OAAtB,CAA8B,KAA9B;MACA,MAAA,CAAO,eAAP,CAAuB,CAAC,gBAAxB,CAAA;aACA,MAAA,CAAO,cAAP,CAAsB,CAAC,gBAAvB,CAAA;IARgD,CAApD;WAUA,EAAA,CAAG,kBAAH,EAAuB,SAAA;AACnB,UAAA;MAAA,UAAA,GAAa,KAAA,CAAM,cAAN,EAAsB,YAAtB;MAEb,CAAA,CAAE,UAAF,CAAa,CAAC,KAAd,CAAA,CAAqB,CAAC,OAAtB,CAA8B,OAA9B;MACA,MAAA,CAAO,UAAP,CAAkB,CAAC,gBAAnB,CAAA;MACA,MAAA,CAAO,CAAA,CAAE,MAAF,CAAS,CAAC,IAAV,CAAA,CAAgB,CAAC,IAAjB,CAAsB,QAAtB,CAAP,CAAuC,CAAC,OAAxC,CAAgD,SAAhD;MACA,MAAA,CAAO,CAAA,CAAE,MAAF,CAAS,CAAC,IAAV,CAAA,CAAgB,CAAC,EAAjB,CAAoB,UAApB,CAAP,CAAsC,CAAC,OAAvC,CAA+C,KAA/C;aACA,MAAA,CAAO,CAAA,CAAE,iCAAF,CAAoC,CAAC,GAArC,CAAA,CAAP,CAAkD,CAAC,OAAnD,CAA2D,QAA3D;IAPmB,CAAvB;EA5B6B,CAAjC;AAAA","file":"postify-spec.js","sourceRoot":"/source/","sourcesContent":["describe \"postify plugin tests\", ->\n a_post = null\n plugin_postify = null\n Postify = null\n\n beforeEach ->\n fixtures = do jasmine.getFixtures\n fixtures.fixturesPath = 'base/test/fixtures/'\n loadFixtures 'postify.html'\n\n a_post = $('a.js-post').postify {csrfToken: \"foobar\", }\n plugin_postify = a_post.first().data 'plugin_postify'\n Postify = $.fn.postify.Postify\n\n it \"doesnt break selector chaining\", ->\n expect(a_post).toEqual $('.js-post')\n expect(a_post.length).toEqual 2\n\n it \"prevents the default click behaviour on click\", ->\n event = {type: 'click', stopPropagation: (->), preventDefault: (->)}\n stopPropagation = spyOn event, 'stopPropagation'\n preventDefault = spyOn event, 'preventDefault'\n\n spyOn plugin_postify, 'formSubmit'\n $(\".js-post\").first().trigger event\n expect(stopPropagation).toHaveBeenCalled()\n expect(preventDefault).toHaveBeenCalled()\n\n it \"submits the form\", ->\n formSubmit = spyOn plugin_postify, 'formSubmit'\n\n $('.js-post').first().trigger 'click'\n expect(formSubmit).toHaveBeenCalled()\n expect($(\"form\").last().attr('action')).toEqual \"/link1/\"\n expect($(\"form\").last().is \":visible\").toEqual false\n expect($(\"input[name=csrfmiddlewaretoken]\").val()).toEqual \"foobar\"\n"]}
{"version":3,"file":"postify-spec.js","sources":["postify-spec.coffee"],"names":[],"mappings":"AAAA;EAAA,QAAA,CAAS,sBAAT,EAAiC,SAAA;AAC7B,QAAA;IAAA,MAAA,GAAS;IACT,cAAA,GAAiB;IACjB,OAAA,GAAU;IAEV,UAAA,CAAW,SAAA;AACP,UAAA;MAAA,QAAA,GAAc,OAAO,CAAC,WAAX,CAAA;MACX,QAAQ,CAAC,YAAT,GAAwB;MACxB,YAAA,CAAa,cAAb;MAEA,MAAA,GAAS,CAAA,CAAE,WAAF,CAAc,CAAC,OAAf,CAAuB;QAAC,SAAA,EAAW,QAAZ;OAAvB;MACT,cAAA,GAAiB,MAAM,CAAC,KAAP,CAAA,CAAc,CAAC,IAAf,CAAoB,gBAApB;aACjB,OAAA,GAAU,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC;IAPhB,CAAX;IASA,EAAA,CAAG,gCAAH,EAAqC,SAAA;MACjC,MAAA,CAAO,MAAP,CAAc,CAAC,OAAf,CAAuB,CAAA,CAAE,UAAF,CAAvB;aACA,MAAA,CAAO,MAAM,CAAC,MAAd,CAAqB,CAAC,OAAtB,CAA8B,CAA9B;IAFiC,CAArC;IAIA,EAAA,CAAG,+CAAH,EAAoD,SAAA;AAChD,UAAA;MAAA,KAAA,GAAQ;QAAC,IAAA,EAAM,OAAP;QAAgB,eAAA,EAAiB,CAAC,SAAA,GAAA,CAAD,CAAjC;QAAuC,cAAA,EAAgB,CAAC,SAAA,GAAA,CAAD,CAAvD;;MACR,eAAA,GAAkB,KAAA,CAAM,KAAN,EAAa,iBAAb;MAClB,cAAA,GAAiB,KAAA,CAAM,KAAN,EAAa,gBAAb;MAEjB,KAAA,CAAM,cAAN,EAAsB,YAAtB;MACA,CAAA,CAAE,UAAF,CAAa,CAAC,KAAd,CAAA,CAAqB,CAAC,OAAtB,CAA8B,KAA9B;MACA,MAAA,CAAO,eAAP,CAAuB,CAAC,gBAAxB,CAAA;aACA,MAAA,CAAO,cAAP,CAAsB,CAAC,gBAAvB,CAAA;IARgD,CAApD;WAUA,EAAA,CAAG,kBAAH,EAAuB,SAAA;AACnB,UAAA;MAAA,UAAA,GAAa,KAAA,CAAM,cAAN,EAAsB,YAAtB;MAEb,CAAA,CAAE,UAAF,CAAa,CAAC,KAAd,CAAA,CAAqB,CAAC,OAAtB,CAA8B,OAA9B;MACA,MAAA,CAAO,UAAP,CAAkB,CAAC,gBAAnB,CAAA;MACA,MAAA,CAAO,CAAA,CAAE,MAAF,CAAS,CAAC,IAAV,CAAA,CAAgB,CAAC,IAAjB,CAAsB,QAAtB,CAAP,CAAuC,CAAC,OAAxC,CAAgD,SAAhD;MACA,MAAA,CAAO,CAAA,CAAE,MAAF,CAAS,CAAC,IAAV,CAAA,CAAgB,CAAC,EAAjB,CAAoB,UAApB,CAAP,CAAsC,CAAC,OAAvC,CAA+C,KAA/C;aACA,MAAA,CAAO,CAAA,CAAE,iCAAF,CAAoC,CAAC,GAArC,CAAA,CAAP,CAAkD,CAAC,OAAnD,CAA2D,QAA3D;IAPmB,CAAvB;EA5B6B,CAAjC;AAAA","sourcesContent":["describe \"postify plugin tests\", ->\n a_post = null\n plugin_postify = null\n Postify = null\n\n beforeEach ->\n fixtures = do jasmine.getFixtures\n fixtures.fixturesPath = 'base/test/fixtures/'\n loadFixtures 'postify.html'\n\n a_post = $('a.js-post').postify {csrfToken: \"foobar\", }\n plugin_postify = a_post.first().data 'plugin_postify'\n Postify = $.fn.postify.Postify\n\n it \"doesnt break selector chaining\", ->\n expect(a_post).toEqual $('.js-post')\n expect(a_post.length).toEqual 2\n\n it \"prevents the default click behaviour on click\", ->\n event = {type: 'click', stopPropagation: (->), preventDefault: (->)}\n stopPropagation = spyOn event, 'stopPropagation'\n preventDefault = spyOn event, 'preventDefault'\n\n spyOn plugin_postify, 'formSubmit'\n $(\".js-post\").first().trigger event\n expect(stopPropagation).toHaveBeenCalled()\n expect(preventDefault).toHaveBeenCalled()\n\n it \"submits the form\", ->\n formSubmit = spyOn plugin_postify, 'formSubmit'\n\n $('.js-post').first().trigger 'click'\n expect(formSubmit).toHaveBeenCalled()\n expect($(\"form\").last().attr('action')).toEqual \"/link1/\"\n expect($(\"form\").last().is \":visible\").toEqual false\n expect($(\"input[name=csrfmiddlewaretoken]\").val()).toEqual \"foobar\"\n"]}

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
{"version":3,"sources":["store-spec.coffee"],"names":[],"mappings":"AAAA;EAAA,QAAA,CAAS,oBAAT,EAA+B,SAAA;AAC3B,QAAA;IAAA,QAAA,GAAW;IACX,OAAA,GAAU;IAEV,UAAA,CAAW,SAAA;AACP,UAAA;MAAA,QAAA,GAAc,OAAO,CAAC,WAAX,CAAA;MACX,QAAQ,CAAC,YAAT,GAAwB;MACxB,YAAA,CAAa,YAAb;MAEG,YAAY,CAAC,KAAhB,CAAA;MACA,QAAA,GAAW,CAAA,CAAE,aAAF,CAAgB,CAAC,KAAjB,CAAuB,WAAvB;aACX,OAAA,GAAU,QAAQ,CAAC,IAAT,CAAc,cAAd;IAPH,CAAX;IASA,EAAA,CAAG,gCAAH,EAAqC,SAAA;aACjC,MAAA,CAAO,QAAP,CAAgB,CAAC,OAAjB,CAAyB,CAAA,CAAE,aAAF,CAAzB;IADiC,CAArC;IAGA,EAAA,CAAG,6BAAH,EAAkC,SAAA;MAC9B,YAAa,CAAA,WAAA,CAAb,GAA4B;MAC5B,QAAA,GAAW,CAAA,CAAE,eAAF,CAAkB,CAAC,KAAnB,CAAyB,WAAzB;aACX,MAAA,CAAO,QAAP,CAAgB,CAAC,WAAjB,CAA6B,MAA7B;IAH8B,CAAlC;IAKA,EAAA,CAAG,qCAAH,EAA0C,SAAA;MACtC,YAAa,CAAA,WAAA,CAAb,GAA4B;MAC5B,CAAA,CAAE,MAAF,CAAS,CAAC,OAAV,CAAkB,SAAlB;aACA,MAAA,CAAO,QAAP,CAAgB,CAAC,WAAjB,CAA6B,MAA7B;IAHsC,CAA1C;IAKA,EAAA,CAAG,sCAAH,EAA2C,SAAA;MACvC,QAAQ,CAAC,GAAT,CAAa,QAAb;MACA,QAAQ,CAAC,OAAT,CAAiB,OAAjB;aACA,MAAA,CAAO,YAAa,CAAA,WAAA,CAApB,CAAiC,CAAC,OAAlC,CAA0C,QAA1C;IAHuC,CAA3C;IAKA,EAAA,CAAG,oCAAH,EAAyC,SAAA;MACrC,KAAA,CAAM,OAAN,EAAe,aAAf;MACA,QAAQ,CAAC,OAAT,CAAiB,OAAjB;aACA,MAAA,CAAO,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,KAA1B,CAAA,CAAP,CAAyC,CAAC,OAA1C,CAAkD,CAAlD;IAHqC,CAAzC;WAKA,EAAA,CAAG,wBAAH,EAA6B,SAAA;AACzB,UAAA;MAAA,YAAa,CAAA,WAAA,CAAb,GAA4B;MAC5B,cAAA,GAAiB,OAAO,CAAC,SAAR,CAAkB,gBAAlB,CAAmC,CAAC,GAAG,CAAC,WAAxC,CAAoD,KAApD;MACjB,KAAA,GAAQ,CAAA,CAAE,MAAF;MACR,KAAK,CAAC,EAAN,CAAS,QAAT,EAAmB,cAAnB;MACA,KAAK,CAAC,OAAN,CAAc,QAAd;MACA,MAAA,CAAO,WAAA,IAAe,YAAtB,CAAmC,CAAC,IAApC,CAAyC,KAAzC;aACA,MAAA,CAAO,cAAP,CAAsB,CAAC,gBAAvB,CAAA;IAPyB,CAA7B;EApC2B,CAA/B;AAAA","file":"store-spec.js","sourceRoot":"/source/","sourcesContent":["describe \"store plugin tests\", ->\n textarea = null\n storage = null\n\n beforeEach ->\n fixtures = do jasmine.getFixtures\n fixtures.fixturesPath = 'base/test/fixtures/'\n loadFixtures 'store.html'\n\n do localStorage.clear\n textarea = $('#my-fixture').store 'unique-id'\n storage = textarea.data 'plugin_store'\n\n it \"doesnt break selector chaining\", ->\n expect(textarea).toEqual $('#my-fixture')\n\n it \"loads previous stored value\", ->\n localStorage['unique-id'] = \"text\"\n textarea = $('#my-fixture-2').store 'unique-id'\n expect(textarea).toHaveValue \"text\"\n\n it \"updates the field on storage change\", ->\n localStorage['unique-id'] = \"text\"\n $(window).trigger \"storage\"\n expect(textarea).toHaveValue \"text\"\n\n it \"saves value to localStorage on input\", ->\n textarea.val \"foobar\"\n textarea.trigger 'input'\n expect(localStorage['unique-id']).toEqual \"foobar\"\n\n it \"wont (re)update the field on input\", ->\n spyOn storage, 'updateField'\n textarea.trigger 'input'\n expect(storage.updateField.calls.count()).toEqual 0\n\n it \"gets cleared on submit\", ->\n localStorage['unique-id'] = \"foo\"\n submitCallback = jasmine.createSpy('submitCallback').and.returnValue false\n $form = $('form')\n $form.on 'submit', submitCallback\n $form.trigger \"submit\"\n expect('unique-id' of localStorage).toBe false\n expect(submitCallback).toHaveBeenCalled()\n"]}
{"version":3,"file":"store-spec.js","sources":["store-spec.coffee"],"names":[],"mappings":"AAAA;EAAA,QAAA,CAAS,oBAAT,EAA+B,SAAA;AAC3B,QAAA;IAAA,QAAA,GAAW;IACX,OAAA,GAAU;IAEV,UAAA,CAAW,SAAA;AACP,UAAA;MAAA,QAAA,GAAc,OAAO,CAAC,WAAX,CAAA;MACX,QAAQ,CAAC,YAAT,GAAwB;MACxB,YAAA,CAAa,YAAb;MAEG,YAAY,CAAC,KAAhB,CAAA;MACA,QAAA,GAAW,CAAA,CAAE,aAAF,CAAgB,CAAC,KAAjB,CAAuB,WAAvB;aACX,OAAA,GAAU,QAAQ,CAAC,IAAT,CAAc,cAAd;IAPH,CAAX;IASA,EAAA,CAAG,gCAAH,EAAqC,SAAA;aACjC,MAAA,CAAO,QAAP,CAAgB,CAAC,OAAjB,CAAyB,CAAA,CAAE,aAAF,CAAzB;IADiC,CAArC;IAGA,EAAA,CAAG,6BAAH,EAAkC,SAAA;MAC9B,YAAa,CAAA,WAAA,CAAb,GAA4B;MAC5B,QAAA,GAAW,CAAA,CAAE,eAAF,CAAkB,CAAC,KAAnB,CAAyB,WAAzB;aACX,MAAA,CAAO,QAAP,CAAgB,CAAC,WAAjB,CAA6B,MAA7B;IAH8B,CAAlC;IAKA,EAAA,CAAG,qCAAH,EAA0C,SAAA;MACtC,YAAa,CAAA,WAAA,CAAb,GAA4B;MAC5B,CAAA,CAAE,MAAF,CAAS,CAAC,OAAV,CAAkB,SAAlB;aACA,MAAA,CAAO,QAAP,CAAgB,CAAC,WAAjB,CAA6B,MAA7B;IAHsC,CAA1C;IAKA,EAAA,CAAG,sCAAH,EAA2C,SAAA;MACvC,QAAQ,CAAC,GAAT,CAAa,QAAb;MACA,QAAQ,CAAC,OAAT,CAAiB,OAAjB;aACA,MAAA,CAAO,YAAa,CAAA,WAAA,CAApB,CAAiC,CAAC,OAAlC,CAA0C,QAA1C;IAHuC,CAA3C;IAKA,EAAA,CAAG,oCAAH,EAAyC,SAAA;MACrC,KAAA,CAAM,OAAN,EAAe,aAAf;MACA,QAAQ,CAAC,OAAT,CAAiB,OAAjB;aACA,MAAA,CAAO,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,KAA1B,CAAA,CAAP,CAAyC,CAAC,OAA1C,CAAkD,CAAlD;IAHqC,CAAzC;WAKA,EAAA,CAAG,wBAAH,EAA6B,SAAA;AACzB,UAAA;MAAA,YAAa,CAAA,WAAA,CAAb,GAA4B;MAC5B,cAAA,GAAiB,OAAO,CAAC,SAAR,CAAkB,gBAAlB,CAAmC,CAAC,GAAG,CAAC,WAAxC,CAAoD,KAApD;MACjB,KAAA,GAAQ,CAAA,CAAE,MAAF;MACR,KAAK,CAAC,EAAN,CAAS,QAAT,EAAmB,cAAnB;MACA,KAAK,CAAC,OAAN,CAAc,QAAd;MACA,MAAA,CAAO,WAAA,IAAe,YAAtB,CAAmC,CAAC,IAApC,CAAyC,KAAzC;aACA,MAAA,CAAO,cAAP,CAAsB,CAAC,gBAAvB,CAAA;IAPyB,CAA7B;EApC2B,CAA/B;AAAA","sourcesContent":["describe \"store plugin tests\", ->\n textarea = null\n storage = null\n\n beforeEach ->\n fixtures = do jasmine.getFixtures\n fixtures.fixturesPath = 'base/test/fixtures/'\n loadFixtures 'store.html'\n\n do localStorage.clear\n textarea = $('#my-fixture').store 'unique-id'\n storage = textarea.data 'plugin_store'\n\n it \"doesnt break selector chaining\", ->\n expect(textarea).toEqual $('#my-fixture')\n\n it \"loads previous stored value\", ->\n localStorage['unique-id'] = \"text\"\n textarea = $('#my-fixture-2').store 'unique-id'\n expect(textarea).toHaveValue \"text\"\n\n it \"updates the field on storage change\", ->\n localStorage['unique-id'] = \"text\"\n $(window).trigger \"storage\"\n expect(textarea).toHaveValue \"text\"\n\n it \"saves value to localStorage on input\", ->\n textarea.val \"foobar\"\n textarea.trigger 'input'\n expect(localStorage['unique-id']).toEqual \"foobar\"\n\n it \"wont (re)update the field on input\", ->\n spyOn storage, 'updateField'\n textarea.trigger 'input'\n expect(storage.updateField.calls.count()).toEqual 0\n\n it \"gets cleared on submit\", ->\n localStorage['unique-id'] = \"foo\"\n submitCallback = jasmine.createSpy('submitCallback').and.returnValue false\n $form = $('form')\n $form.on 'submit', submitCallback\n $form.trigger \"submit\"\n expect('unique-id' of localStorage).toBe false\n expect(submitCallback).toHaveBeenCalled()\n"]}

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
{"version":3,"sources":["util-spec.coffee"],"names":[],"mappings":"AAAA;EAAA,QAAA,CAAS,mBAAT,EAA8B,SAAA;WAE1B,EAAA,CAAG,kBAAH,EAAuB,SAAA;AACnB,UAAA;MAAA,MAAA,GAAS,CAAC,CAAC,MAAF,CAAS,0BAAT,EAAqC;QAAC,GAAA,EAAK,UAAN;QAAkB,GAAA,EAAK,UAAvB;OAArC;aACT,MAAA,CAAO,MAAP,CAAc,CAAC,OAAf,CAAuB,gCAAvB;IAFmB,CAAvB;EAF0B,CAA9B;AAAA","file":"util-spec.js","sourceRoot":"/source/","sourcesContent":["describe \"util format tests\", ->\n\n it \"formats a string\", ->\n result = $.format \"{foo} foobar {bar} {bad}\", {foo: \"foo text\", bar: \"bar text\"}\n expect(result).toEqual \"foo text foobar bar text {bad}\"\n"]}
{"version":3,"file":"util-spec.js","sources":["util-spec.coffee"],"names":[],"mappings":"AAAA;EAAA,QAAA,CAAS,mBAAT,EAA8B,SAAA;WAE1B,EAAA,CAAG,kBAAH,EAAuB,SAAA;AACnB,UAAA;MAAA,MAAA,GAAS,CAAC,CAAC,MAAF,CAAS,0BAAT,EAAqC;QAAC,GAAA,EAAK,UAAN;QAAkB,GAAA,EAAK,UAAvB;OAArC;aACT,MAAA,CAAO,MAAP,CAAc,CAAC,OAAf,CAAuB,gCAAvB;IAFmB,CAAvB;EAF0B,CAA9B;AAAA","sourcesContent":["describe \"util format tests\", ->\n\n it \"formats a string\", ->\n result = $.format \"{foo} foobar {bar} {bad}\", {foo: \"foo text\", bar: \"bar text\"}\n expect(result).toEqual \"foo text foobar bar text {bad}\"\n"]}

View File

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.conf import settings as django_settings
from .registry import register
@register.simple_tag(takes_context=True)
def load_settings(context, *settings):
context['st_settings'] = {
setting: getattr(django_settings, setting)
for setting in settings}
return ''

View File

@ -12,6 +12,7 @@ from ..tags import avatar
from ..tags import gravatar
from ..tags import messages
from ..tags import paginator
from ..tags import settings
from ..tags import social_share
from ..tags import time
from ..tags import urls
@ -30,6 +31,7 @@ __all__ = [
'gravatar',
'messages',
'paginator',
'settings',
'social_share',
'time',
'urls',

View File

@ -33,15 +33,16 @@ ST_PRIVATE_FORUM = False
# if that file contains a valid PNG header
# followed by malicious HTML. See:
# https://docs.djangoproject.com/en/1.11/topics/security/#user-uploaded-content
ST_ALLOWED_UPLOAD_IMAGE_FORMAT = ('jpeg', 'gif')
ST_ALLOWED_UPLOAD_IMAGE_FORMAT = ('jpeg', 'jpg', 'gif')
ST_UPLOAD_IMAGE_ENABLED = True
# Only media types are allowed:
# https://www.iana.org/assignments/media-types/media-types.xhtml
ST_ALLOWED_UPLOAD_FILE_MEDIA_TYPE = OrderedDict([
('doc', 'application/msword'), # .doc
('docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'), # .docx
('pdf', 'application/pdf'),
])
ST_ALLOWED_UPLOAD_FILE_MEDIA_TYPE = {
'doc': 'application/msword',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'pdf': 'application/pdf'}
ST_UPLOAD_FILE_ENABLED = True
ST_ALLOWED_URL_PROTOCOLS = {
'http', 'https', 'mailto', 'ftp', 'ftps',

2820
yarn.lock Normal file

File diff suppressed because it is too large Load Diff