Commit 0790a3ad by Rocky Duan

image uploading & render markdown

parent 7a53bc52
......@@ -28,7 +28,7 @@
// The text that appears on the upper part of the dialog box when
// entering links.
var linkDialogText = "<p><b>Insert Hyperlink</b></p><p>http://example.com/ \"optional title\"</p>";
var imageDialogText = "<p><b>Insert Image</b></p><p>http://example.com/images/diagram.jpg \"optional title\"<br><br>Need <a href='http://www.google.com/search?q=free+image+hosting' target='_blank'>free image hosting?</a></p>";
var imageDialogText = "<p><b>Insert Image (upload file or type url)</b></p><p>http://example.com/images/diagram.jpg \"optional title\"<br><br></p>";
// The default text that appears in the dialog input box when entering
// links.
......@@ -49,7 +49,7 @@
// - getConverter() returns the markdown converter object that was passed to the constructor
// - run() actually starts the editor; should be called after all necessary plugins are registered. Calling this more than once is a no-op.
// - refreshPreview() forces the preview to be updated. This method is only available after run() was called.
Markdown.Editor = function (markdownConverter, idPostfix, help) {
Markdown.Editor = function (markdownConverter, idPostfix, help, imageUploadHandler) {
idPostfix = idPostfix || "";
......@@ -88,7 +88,7 @@
}
}
uiManager = new UIManager(idPostfix, panels, undoManager, previewManager, commandManager, help);
uiManager = new UIManager(idPostfix, panels, undoManager, previewManager, commandManager, help, imageUploadHandler);
uiManager.setUndoRedoButtonStates();
var forceRefresh = that.refreshPreview = function () { previewManager.refresh(true); };
......@@ -1010,7 +1010,7 @@
// callback: The function which is executed when the prompt is dismissed, either via OK or Cancel.
// It receives a single argument; either the entered text (if OK was chosen) or null (if Cancel
// was chosen).
ui.prompt = function (text, defaultInputText, callback) {
ui.prompt = function (text, defaultInputText, callback, imageUploadHandler) {
// These variables need to be declared at this level since they are used
// in multiple functions.
......@@ -1044,8 +1044,10 @@
else {
// Fixes common pasting errors.
text = text.replace(/^http:\/\/(https?|ftp):\/\//, '$1://');
if (!/^(?:https?|ftp):\/\//.test(text))
// doesn't change url if started with '/' (local)
if (!/^(?:https?|ftp):\/\//.test(text) && text.charAt(0) != '/') {
text = 'http://' + text;
}
}
dialog.parentNode.removeChild(dialog);
......@@ -1095,6 +1097,21 @@
style.marginLeft = style.marginRight = "auto";
form.appendChild(input);
// The choose file button if prompt type is 'image'
if (imageUploadHandler) {
var chooseFile = doc.createElement("input");
chooseFile.type = "file";
chooseFile.name = "file-upload";
chooseFile.id = "file-upload";
chooseFile.onchange = function() {
imageUploadHandler(this, input);
};
form.appendChild(doc.createElement("br"));
form.appendChild(chooseFile);
}
// The ok button
var okButton = doc.createElement("input");
okButton.type = "button";
......@@ -1160,7 +1177,7 @@
}, 0);
};
function UIManager(postfix, panels, undoManager, previewManager, commandManager, helpOptions) {
function UIManager(postfix, panels, undoManager, previewManager, commandManager, helpOptions, imageUploadHandler) {
var inputBox = panels.input,
buttons = {}; // buttons.undo, buttons.link, etc. The actual DOM elements.
......@@ -1419,7 +1436,7 @@
buttons.quote = makeButton("wmd-quote-button", "Blockquote <blockquote> Ctrl+Q", "-60px", bindCommand("doBlockquote"));
buttons.code = makeButton("wmd-code-button", "Code Sample <pre><code> Ctrl+K", "-80px", bindCommand("doCode"));
buttons.image = makeButton("wmd-image-button", "Image <img> Ctrl+G", "-100px", bindCommand(function (chunk, postProcessing) {
return this.doLinkOrImage(chunk, postProcessing, true);
return this.doLinkOrImage(chunk, postProcessing, true, imageUploadHandler);
}));
makeSpacer(2);
buttons.olist = makeButton("wmd-olist-button", "Numbered List <ol> Ctrl+O", "-120px", bindCommand(function (chunk, postProcessing) {
......@@ -1649,7 +1666,7 @@
});
}
commandProto.doLinkOrImage = function (chunk, postProcessing, isImage) {
commandProto.doLinkOrImage = function (chunk, postProcessing, isImage, imageUploadHandler) {
chunk.trimWhitespace();
chunk.findTags(/\s*!?\[/, /\][ ]?(?:\n[ ]*)?(\[.*?\])?/);
......@@ -1724,7 +1741,7 @@
if (isImage) {
if (!this.hooks.insertImageDialog(linkEnteredCallback))
ui.prompt(imageDialogText, imageDefaultText, linkEnteredCallback);
ui.prompt(imageDialogText, imageDefaultText, linkEnteredCallback, imageUploadHandler);
}
else {
ui.prompt(linkDialogText, linkDefaultText, linkEnteredCallback);
......
jQuery.extend({
handleError: function( s, xhr, status, e ) {
// If a local callback was specified, fire it
if ( s.error ) {
s.error.call( s.context || s, xhr, status, e );
}
// Fire the global callback
if ( s.global ) {
(s.context ? jQuery(s.context) : jQuery.event).trigger( "ajaxError", [xhr, s, e] );
}
},
createUploadIframe: function(id, uri){
//create frame
var frameId = 'jUploadFrame' + id;
if(window.ActiveXObject) {
var io = document.createElement('<iframe id="' + frameId + '" name="' + frameId + '" />');
if(typeof uri== 'boolean'){
io.src = 'javascript:false';
}
else if(typeof uri== 'string'){
io.src = uri;
}
}
else {
var io = document.createElement('iframe');
io.id = frameId;
io.name = frameId;
}
io.style.position = 'absolute';
io.style.top = '-1000px';
io.style.left = '-1000px';
document.body.appendChild(io);
return io;
},
createUploadForm: function(id, fileElementId)
{
//create form
var formId = 'jUploadForm' + id;
var fileId = 'jUploadFile' + id;
var form = $('<form action="" method="POST" name="' + formId + '" id="' + formId
+ '" enctype="multipart/form-data"></form>');
var oldElement = $('#' + fileElementId);
var newElement = $(oldElement).clone();
$(oldElement).attr('id', fileId);
$(oldElement).before(newElement);
$(oldElement).appendTo(form);
//set attributes
$(form).css('position', 'absolute');
$(form).css('top', '-1200px');
$(form).css('left', '-1200px');
$(form).appendTo('body');
return form;
},
ajaxFileUpload: function(s) {
// TODO introduce global settings, allowing the client to modify them for all requests, not only timeout
s = jQuery.extend({}, jQuery.ajaxSettings, s);
var id = new Date().getTime()
var form = jQuery.createUploadForm(id, s.fileElementId);
var io = jQuery.createUploadIframe(id, s.secureuri);
var frameId = 'jUploadFrame' + id;
var formId = 'jUploadForm' + id;
// Watch for a new set of requests
if ( s.global && ! jQuery.active++ )
{
jQuery.event.trigger( "ajaxStart" );
}
var requestDone = false;
// Create the request object
var xml = {}
if ( s.global )
jQuery.event.trigger("ajaxSend", [xml, s]);
// Wait for a response to come back
var uploadCallback = function(isTimeout)
{
var io = document.getElementById(frameId);
try {
if(io.contentWindow){
xml.responseText = io.contentWindow.document.body ?
io.contentWindow.document.body.innerText : null;
xml.responseXML = io.contentWindow.document.XMLDocument ?
io.contentWindow.document.XMLDocument : io.contentWindow.document;
}
else if(io.contentDocument)
{
xml.responseText = io.contentDocument.document.body ?
io.contentDocument.document.body.textContent || document.body.innerText : null;
xml.responseXML = io.contentDocument.document.XMLDocument ?
io.contentDocument.document.XMLDocument : io.contentDocument.document;
}
}
catch(e)
{
jQuery.handleError(s, xml, null, e);
}
if ( xml || isTimeout == "timeout")
{
requestDone = true;
var status;
try {
status = isTimeout != "timeout" ? "success" : "error";
// Make sure that the request was successful or notmodified
if ( status != "error" )
{
// process the data (runs the xml through httpData regardless of callback)
var data = jQuery.uploadHttpData( xml, s.dataType );
// If a local callback was specified, fire it and pass it the data
if ( s.success )
s.success( data, status );
// Fire the global callback
if( s.global )
jQuery.event.trigger( "ajaxSuccess", [xml, s] );
} else
jQuery.handleError(s, xml, status);
} catch(e)
{
status = "error";
jQuery.handleError(s, xml, status, e);
}
// The request was completed
if( s.global )
jQuery.event.trigger( "ajaxComplete", [xml, s] );
// Handle the global AJAX counter
if ( s.global && ! --jQuery.active )
jQuery.event.trigger( "ajaxStop" );
// Process result
if ( s.complete )
s.complete(xml, status);
jQuery(io).unbind();
setTimeout(function()
{ try
{
$(io).remove();
$(form).remove();
} catch(e) {
jQuery.handleError(s, xml, null, e);
}
}, 100)
xml = null;
}
}
// Timeout checker
if ( s.timeout > 0 ) {
setTimeout(function(){
// Check to see if the request is still happening
if( !requestDone ) uploadCallback( "timeout" );
}, s.timeout);
}
try
{
// var io = $('#' + frameId);
var form = $('#' + formId);
$(form).attr('action', s.url);
$(form).attr('method', 'POST');
$(form).attr('target', frameId);
if(form.encoding)
{
form.encoding = 'multipart/form-data';
}
else
{
form.enctype = 'multipart/form-data';
}
$(form).submit();
} catch(e)
{
jQuery.handleError(s, xml, null, e);
}
if(window.attachEvent){
document.getElementById(frameId).attachEvent('onload', uploadCallback);
}
else{
document.getElementById(frameId).addEventListener('load', uploadCallback, false);
}
return {abort: function () {}};
},
uploadHttpData: function( r, type ) {
var data = !type;
data = type == "xml" || data ? r.responseXML : r.responseText;
// If the type is "script", eval it in global context
if ( type == "script" )
jQuery.globalEval( data );
// Get the JavaScript object, if JSON is used.
if ( type == "json" )
eval( "data = " + data );
// evaluate scripts within html
if ( type == "html" )
jQuery("<div>").html(data).evalScripts();
//alert($('param', data).each(function(){alert($(this).attr('value'));}));
return data;
}
})
......@@ -3,6 +3,7 @@ import django_comment_client.base.views
urlpatterns = patterns('django_comment_client.base.views',
url(r'upload$', 'upload', name='upload'),
url(r'threads/(?P<thread_id>[\w\-]+)/update$', 'update_thread', name='update_thread'),
url(r'threads/(?P<thread_id>[\w\-]+)/reply$', 'create_comment', name='create_comment'),
url(r'threads/(?P<thread_id>[\w\-]+)/delete', 'delete_thread', name='delete_thread'),
......
import time
import random
import os
import os.path
import logging
import urlparse
import comment_client
from django.core import exceptions
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_POST, require_GET
from django.http import HttpResponse
from django.utils import simplejson
import comment_client
from django.views.decorators import csrf
from django.core.files.storage import get_storage_class
from django.utils.translation import ugettext as _
from django.conf import settings
class JsonResponse(HttpResponse):
def __init__(self, data=None):
......@@ -183,3 +195,82 @@ def search(request, course_id):
commentable_id = request.GET.get('commentable_id', None)
response = comment_client.search(text, commentable_id)
return JsonResponse(response)
@csrf.csrf_exempt
@login_required
@require_POST
def upload(request, course_id):#ajax upload file to a question or answer
"""view that handles file upload via Ajax
"""
# check upload permission
result = ''
error = ''
new_file_name = ''
try:
# TODO authorization
#may raise exceptions.PermissionDenied
#if request.user.is_anonymous():
# msg = _('Sorry, anonymous users cannot upload files')
# raise exceptions.PermissionDenied(msg)
#request.user.assert_can_upload_file()
# check file type
f = request.FILES['file-upload']
file_extension = os.path.splitext(f.name)[1].lower()
if not file_extension in settings.DISCUSSION_ALLOWED_UPLOAD_FILE_TYPES:
file_types = "', '".join(settings.DISCUSSION_ALLOWED_UPLOAD_FILE_TYPES)
msg = _("allowed file types are '%(file_types)s'") % \
{'file_types': file_types}
raise exceptions.PermissionDenied(msg)
# generate new file name
new_file_name = str(
time.time()
).replace(
'.',
str(random.randint(0,100000))
) + file_extension
file_storage = get_storage_class()()
# use default storage to store file
file_storage.save(new_file_name, f)
# check file size
# byte
size = file_storage.size(new_file_name)
if size > settings.ASKBOT_MAX_UPLOAD_FILE_SIZE:
file_storage.delete(new_file_name)
msg = _("maximum upload file size is %(file_size)sK") % \
{'file_size': settings.ASKBOT_MAX_UPLOAD_FILE_SIZE}
raise exceptions.PermissionDenied(msg)
except exceptions.PermissionDenied, e:
error = unicode(e)
except Exception, e:
logging.critical(unicode(e))
error = _('Error uploading file. Please contact the site administrator. Thank you.')
if error == '':
result = 'Good'
file_url = file_storage.url(new_file_name)
parsed_url = urlparse.urlparse(file_url)
file_url = urlparse.urlunparse(
urlparse.ParseResult(
parsed_url.scheme,
parsed_url.netloc,
parsed_url.path,
'', '', ''
)
)
else:
result = ''
file_url = ''
return JsonResponse({
'result': {
'msg': result,
'error': error,
'file_url': file_url,
}
})
......@@ -30,6 +30,7 @@ import djcelery
from path import path
from .askbotsettings import * # this is where LIVESETTINGS_OPTIONS comes from
from .discussionsettings import *
################################### FEATURES ###################################
COURSEWARE_ENABLED = True
......
DISCUSSION_ALLOWED_UPLOAD_FILE_TYPES = ('.jpg', '.jpeg', '.gif', '.bmp', '.png', '.tiff')
......@@ -102,6 +102,7 @@ $ ->
text
if Markdown?
Markdown.getMathCompatibleConverter = ->
converter = Markdown.getSanitizingConverter()
processor = new MathJaxProcessor()
......@@ -109,11 +110,13 @@ $ ->
converter.hooks.chain "postConversion", processor.replaceMath
converter
Markdown.makeWmdEditor = (elem, appended_id) ->
Markdown.makeWmdEditor = (elem, appended_id, imageUploadUrl) ->
$elem = $(elem)
if not $elem.length
console.log "warning: elem for makeWmdEditor doesn't exist"
return
if not $elem.find(".wmd-panel").length
_append = appended_id || ""
$wmdPanel = $("<div>").addClass("wmd-panel")
......@@ -121,8 +124,42 @@ $ ->
.append($("<textarea>").addClass("wmd-input").attr("id", "wmd-input#{_append}"))
.append($("<div>").attr("id", "wmd-preview#{_append}").addClass("wmd-panel wmd-preview"))
$elem.append($wmdPanel)
converter = Markdown.getMathCompatibleConverter()
editor = new Markdown.Editor(converter, appended_id)
ajaxFileUpload = (imageUploadUrl, input, startUploadHandler) ->
$("#loading").ajaxStart(-> $(this).show()).ajaxComplete(-> $(this).hide())
$("#upload").ajaxStart(-> $(this).hide()).ajaxComplete(-> $(this).show())
$.ajaxFileUpload
url: imageUploadUrl
secureuri: false
fileElementId: 'file-upload'
dataType: 'json'
success: (data, status) ->
fileURL = data['result']['file_url']
error = data['result']['error']
if error != ''
alert error
if startUploadHandler
$('#file-upload').unbind('change').change(startUploadHandler)
console.log error
else
$(input).attr('value', fileURL)
error: (data, status, e) ->
alert(e)
if startUploadHandler
$('#file-upload').unbind('change').change(startUploadHandler)
imageUploadHandler = (elem, input) ->
console.log "here"
ajaxFileUpload(imageUploadUrl, input, imageUploadHandler)
editor = new Markdown.Editor(
converter,
appended_id, # idPostfix
null, # help handler
imageUploadHandler
)
delayRenderer = new MathJaxDelayRenderer()
editor.hooks.chain "onPreviewPush", (text, previewSet) ->
delayRenderer.render
......
......@@ -43,6 +43,7 @@ Discussion =
delete_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/delete"
upvote_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/upvote"
downvote_comment : "/courses/#{$$course_id}/discussion/comments/#{param}/downvote"
upload : "/courses/#{$$course_id}/discussion/upload"
search : "/courses/#{$$course_id}/discussion/forum/search"
}[name]
......@@ -89,7 +90,7 @@ Discussion =
newPostBody = $(discussion).find(".new-post-body")
if newPostBody.length
Markdown.makeWmdEditor newPostBody, "-new-post-body-#{$(discussion).attr('_id')}"
Markdown.makeWmdEditor newPostBody, "-new-post-body-#{$(discussion).attr('_id')}", Discussion.urlFor('upload')
initializeWatchThreads = (index, thread) ->
$thread = $(thread)
......@@ -175,7 +176,7 @@ Discussion =
$discussionContent.append(editView)
Markdown.makeWmdEditor $local(".comment-edit"), "-comment-edit-#{id}"
Markdown.makeWmdEditor $local(".comment-edit"), "-comment-edit-#{id}", Discussion.urlFor('upload')
cancelReply = generateDiscussionLink("discussion-cancel-reply", "Cancel", handleCancelReply)
submitReply = generateDiscussionLink("discussion-submit-reply", "Submit", handleSubmitReply)
$local(".discussion-link").hide()
......@@ -230,10 +231,17 @@ Discussion =
$local(".discussion-vote-down").click ->
handleVote(this, "down")
initializeContent: (content) ->
$content = $(content)
$local = generateLocal($content.children(".discussion-content"))
raw_text = $local(".content-body").html()
converter = Markdown.getMathCompatibleConverter()
$local(".content-body").html(converter.makeHtml(raw_text))
bindDiscussionEvents: (discussion) ->
$discussion = $(discussion)
$discussionNonContent = $discussion.children(".discussion-non-content")
$local = (selector) -> $discussionNonContent.find(selector)
$local = generateLocal($discussionNonContent)#(selector) -> $discussionNonContent.find(selector)
id = $discussion.attr("_id")
......@@ -266,7 +274,9 @@ Discussion =
$local(".new-post-form").submit()
$discussion.find(".thread").each (index, thread) ->
Discussion.initializeContent(thread)
Discussion.bindContentEvents(thread)
$discussion.find(".comment").each (index, comment) ->
Discussion.initializeContent(comment)
Discussion.bindContentEvents(comment)
......@@ -28,6 +28,7 @@
MathJax extension libraries -->
<script type="text/javascript" src="/static/js/vendor/mathjax-MathJax-c9db6ac/MathJax.js?config=TeX-MML-AM_HTMLorMML-full"></script>
<script type="text/javascript" src="${static.url('js/vendor/split.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/jquery.ajaxfileupload.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/Markdown.Converter.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/Markdown.Sanitizer.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/Markdown.Editor.js')}"></script>
......
......@@ -15,9 +15,9 @@
<div class="discussion-upper-wrapper clearfix">
${render_vote(thread)}
<div class="discussion-right-wrapper clearfix">
<a class="thread-title" name="${thread['id']}" href="${url_for_thread}">${thread['title']}</a>
<a class="thread-title" name="${thread['id']}" href="${url_for_thread}">${thread['title'] | h}</a>
<div class="discussion-content-view">
<div class="thread-body">${thread['body']}</div>
<div class="content-body thread-body">${thread['body'] | h}</div>
<div class="info">
${render_info(thread)}
% if edit_thread:
......@@ -45,7 +45,7 @@
${render_vote(comment)}
<div class="discussion-right-wrapper">
<div class="discussion-content-view">
<a class="comment-body" name="${comment['id']}">${comment['body']}</a>
<a class="content-body comment-body" name="${comment['id']}">${comment['body'] | h}</a>
<div class="info">
${render_info(comment)}
${render_reply()}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment