Commit 7cab7df9 authored by fred's avatar fred

add draft upload of sound files

parent 2ff2637e
import datetime
import re
import unicodedata
import os
import uuid
from django import forms
from django.forms import fields
from .models import Emission, Episode, Diffusion, Schedule
from django.core.files.storage import DefaultStorage
from django.core.urlresolvers import reverse
from django.utils.safestring import mark_safe
from django.conf import settings
from django.template.loader import render_to_string
from .models import Emission, Episode, Diffusion, Schedule, SoundFile
def slugify(s):
s = unicodedata.normalize('NFKD', s).encode('ascii', 'ignore').lower()
......@@ -40,6 +50,74 @@ class DayAndHourWidget(forms.MultiWidget):
return None
class JqueryFileUploadFileInput(forms.FileInput):
def render(self, name, value, attrs=None):
output = render_to_string('emissions/upload.html', {
'upload_url': self.url,
'files': self.files,
'name': name,
'STATIC_URL': settings.STATIC_URL})
return mark_safe(output)
class JqueryFileUploadInput(forms.MultiWidget):
needs_multipart_form = True
upload_id_re = re.compile(r'^[a-z0-9A-Z-]+$')
upload_id = None
def __init__(self, attrs=None, choices=[], max_filename_length=None):
self.max_filename_length = max_filename_length
widget_list = (forms.HiddenInput(attrs=attrs),
JqueryFileUploadFileInput(attrs=attrs))
super(JqueryFileUploadInput, self).__init__(widget_list, attrs)
def decompress(self, value):
# map python value to widget contents
if self.upload_id:
pass
elif isinstance(value, (list, tuple)) and value and value[0] is not None:
self.upload_id = str(value[0])
else:
self.upload_id = str(uuid.uuid4())
return [self.upload_id, None]
def get_files_for_id(self, upload_id):
storage = DefaultStorage()
path = os.path.join('upload', upload_id)
if not storage.exists(path):
return
for filepath in storage.listdir(path)[1]:
name = os.path.basename(filepath)
yield storage.open(os.path.join(path, name))
def value_from_datadict(self, data, files, name):
'''
If some file was submitted, that's the value,
If a regular hidden_id is present, use it to find uploaded files,
otherwise return an empty list
'''
upload_id, file_input = super(JqueryFileUploadInput, self).value_from_datadict(data, files, name)
if file_input:
pass
elif JqueryFileUploadInput.upload_id_re.match(upload_id):
file_input = list(self.get_files_for_id(upload_id))
else:
file_input = []
return file_input[0]
def render(self, name, value, attrs=None):
self.decompress(value)
self.widgets[1].url = '/upload/%s/' % self.upload_id
self.widgets[1].url = reverse('upload', kwargs={'transaction_id': self.upload_id})
if self.max_filename_length:
self.widgets[1].url += '?max_filename_length=%d' % self.max_filename_length
self.widgets[1].files = '/upload/%s/' % self.get_files_for_id(self.upload_id)
output = super(JqueryFileUploadInput, self).render(name, value,
attrs)
fileinput_id = '%s_%s' % (attrs['id'], '1')
return output
class EmissionForm(forms.ModelForm):
class Meta:
model = Emission
......@@ -77,3 +155,12 @@ class ScheduleForm(forms.ModelForm):
'emission': forms.HiddenInput(),
'datetime': DayAndHourWidget(),
}
class SoundFileForm(forms.ModelForm):
class Meta:
model = SoundFile
widgets = {
'episode': forms.HiddenInput(),
'file': JqueryFileUploadInput(),
}
......@@ -67,3 +67,11 @@ class Episode(models.Model):
class Diffusion(models.Model):
episode = models.ForeignKey('Episode', verbose_name=u'Episode')
datetime = models.DateTimeField()
class SoundFile(models.Model):
episode = models.ForeignKey('Episode', verbose_name=u'Episode')
file = models.FileField(upload_to='sounds', max_length=250)
podcastable = models.BooleanField(default=False)
fragment = models.BooleanField(default=False)
title = models.CharField(max_length=50)
......@@ -26,6 +26,49 @@
{% endfor %}
</ul>
<h3>Sons</h3>
<table>
<thead>
<tr>
<th>Fichier</th>
<th>Titre</th>
<th>Podcastable?</th>
<th>Fragment?</th>
</thead>
<tbody>
{% for soundfile in soundfiles %}
<tr>
<td><a href="{{ soundfile.file.url }}">{{ soundfile.file.name }}</a></td>
<td>{{ soundfile.title }}</td>
<td>{{ soundfile.podcastable }}</td>
<td>{{ soundfile.fragment }}</td>
{% endfor %}
</tbody>
<tfoot>
<tr><td rowspan="4"><a id="add-soundfile-link" href="#">Ajouter un son</a></td></tr>
</tfoot>
</table>
<form id="add-soundfile-form" action="add-soundfile" method="POST" style="display: none;">
{% csrf_token %}
{{ add_soundfile_form.as_p }}
<input type="submit" value="Ajouter ce son"/>
</form>
<a href="edit/">Edit</a>
{% endblock %}
{% block page-end %}
<script>
$(function() {
$('#add-soundfile-link').click(
function() {
$('#add-soundfile-form').dialog({modal: true, title: 'Son', width: 'auto'});
});
});
</script>
{% endblock %}
{% extends "panikdb/base.html" %}
{% block extrascripts %}
<script src="{{ STATIC_URL }}ckeditor/ckeditor/ckeditor.js">
</script>
{% endblock %}
{% block content %}
<form method="post">
<div id="form-content">
{% csrf_token %}
{{ form.as_p }}
</div>
{% block buttons %}
<button class="enable-on-change">Modifier</button>
{% endblock %}
</form>
{% endblock %}
{% load i18n %}
<div id="fileupload" class="file-upload-widget">
<div class="fileupload-buttonbar">
<label class="fileinput-button">
<input type="file" name="{{ name }}" data-url="{{ upload_url }}">
<span class="fileinfo"><span class="filename"/></span>
</label>
</div>
<div class="fileupload-content">
<div class="fileprogress" style="display: none;">
<div class="bar">
Upload en cours…
</div>
</div>
</div>
</div>
......@@ -7,6 +7,9 @@ urlpatterns = patterns('',
url(r'^categories$', CategoryListView.as_view(), name='category-list'),
url(r'^days$', DaysView.as_view(), name='days'),
url(r'^add$', EmissionCreateView.as_view(), name='emission-add'),
url(r'^upload/(?P<transaction_id>[a-zA-Z0-9-]+)/$', UploadView.as_view(), name='upload'),
url(r'^(?P<slug>[\w,-]+)/$', EmissionDetailView.as_view(), name='emission-view'),
url(r'^(?P<slug>[\w,-]+)/edit/$', EmissionUpdateView.as_view(), name='emission-update'),
url(r'^(?P<slug>[\w,-]+)/delete/$', EmissionDeleteView.as_view(), name='emission-delete'),
......@@ -19,5 +22,7 @@ urlpatterns = patterns('',
url(r'^(?P<emission_slug>[\w,-]+)/(?P<slug>[\w,-]+)/$', EpisodeDetailView.as_view(), name='episode-view'),
url(r'^(?P<emission_slug>[\w,-]+)/(?P<slug>[\w,-]+)/edit/$', EpisodeUpdateView.as_view(), name='episode-update'),
url(r'^(?P<emission_slug>[\w,-]+)/(?P<slug>[\w,-]+)/delete/$', EpisodeDeleteView.as_view(), name='episode-delete'),
url(r'^(?P<emission_slug>[\w,-]+)/(?P<slug>[\w,-]+)/add-soundfile$',
EpisodeAddSoundFileView.as_view(), name='episode-add-soundfile'),
)
import datetime
import os
from django.core.files.storage import DefaultStorage
from django.http import HttpResponse, Http404
from django.core.urlresolvers import reverse, reverse_lazy
from django.utils import simplejson
from django.views.generic.base import TemplateView, RedirectView
from django.views.decorators.csrf import csrf_exempt
from django.views.generic.base import View, TemplateView, RedirectView
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.views.generic.list import ListView
from django.views.generic.detail import DetailView
from .models import Emission, Episode, Diffusion, Category, Schedule
from .forms import EmissionForm, EpisodeForm, EpisodeNewForm, ScheduleForm
from .models import Emission, Episode, Diffusion, Category, Schedule, SoundFile
from .forms import EmissionForm, EpisodeForm, EpisodeNewForm, ScheduleForm, \
SoundFileForm
__all__ = ['EmissionListView', 'EmissionDetailView', 'EmissionCreateView',
'EmissionUpdateView', 'EmissionDeleteView',
'EpisodeCreateView', 'EpisodeDetailView', 'EpisodeUpdateView',
'EpisodeDeleteView', 'EmissionAddScheduleView',
'ScheduleDeleteView', 'CategoryListView', 'DaysView']
'ScheduleDeleteView', 'CategoryListView', 'DaysView',
'UploadView', 'EpisodeAddSoundFileView']
class EmissionListView(ListView):
......@@ -90,10 +97,11 @@ class EpisodeDetailView(DetailView):
def get_context_data(self, **kwargs):
context = super(EpisodeDetailView, self).get_context_data(**kwargs)
context['diffusions'] = Diffusion.objects.filter(episode=self.object.id)
context['soundfiles'] = SoundFile.objects.filter(episode=self.object.id)
context['add_soundfile_form'] = SoundFileForm(initial={'episode': self.object})
return context
class EpisodeUpdateView(UpdateView):
form_class = EpisodeForm
model = Episode
......@@ -129,3 +137,45 @@ class DaysView(TemplateView):
'datetime': datetime.datetime(2007, 1, day+1)})
context['days'] = days
return context
class JSONResponse(HttpResponse):
"""JSON response class."""
def __init__(self, obj='', json_opts={}, mimetype='application/json', *args, **kwargs):
content = simplejson.dumps(obj, **json_opts)
super(JSONResponse,self).__init__(content, mimetype, *args, **kwargs)
class UploadView(View):
def response_mimetype(self, request):
if 'application/json' in request.META['HTTP_ACCEPT']:
return 'application/json'
else:
return 'text/plain'
@csrf_exempt
def post(self, request, transaction_id):
storage = DefaultStorage()
max_filename_length = 256
url = reverse('upload', kwargs={'transaction_id': transaction_id})
if request.FILES is None:
response = JSONResponse({}, {}, self.response_mimetype(request))
response['Content-Disposition'] = 'inline; filename=files.json'
return response
data = []
for uploaded_file in request.FILES.values():
path = os.path.join('upload', str(transaction_id), uploaded_file.name)
filename = storage.save(path, uploaded_file)
url = '%s%s' % (url, os.path.basename(filename))
data.append({'name': uploaded_file.name, 'size': uploaded_file.size, 'url': url})
response = JSONResponse(data, {}, self.response_mimetype(request))
response['Content-Disposition'] = 'inline; filename=files.json'
return response
class EpisodeAddSoundFileView(CreateView):
form_class = SoundFileForm
model = SoundFile
def get_success_url(self):
return self.object.episode.get_absolute_url()
......@@ -332,6 +332,22 @@ ul.episode-list {
-webkit-column-count: 2;
}
.fileprogress {
border: 1px solid #888;
}
.fileprogress .bar {
background: #09f;
line-height: 1.5em;
padding-left: 1ex;
font-weight: bold;
white-space: nowrap;
}
.fileinfo {
line-height: 1.5em;
}
[class^="icon-"]:before, [class*=" icon-"]:before {
font-family: FontAwesome;
font-weight: normal;
......
This diff is collapsed.
/*
* jQuery Iframe Transport Plugin 1.6.1
* https://github.com/blueimp/jQuery-File-Upload
*
* Copyright 2011, Sebastian Tschan
* https://blueimp.net
*
* Licensed under the MIT license:
* http://www.opensource.org/licenses/MIT
*/
/*jslint unparam: true, nomen: true */
/*global define, window, document */
(function (factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
// Register as an anonymous AMD module:
define(['jquery'], factory);
} else {
// Browser globals:
factory(window.jQuery);
}
}(function ($) {
'use strict';
// Helper variable to create unique names for the transport iframes:
var counter = 0;
// The iframe transport accepts three additional options:
// options.fileInput: a jQuery collection of file input fields
// options.paramName: the parameter name for the file form data,
// overrides the name property of the file input field(s),
// can be a string or an array of strings.
// options.formData: an array of objects with name and value properties,
// equivalent to the return data of .serializeArray(), e.g.:
// [{name: 'a', value: 1}, {name: 'b', value: 2}]
$.ajaxTransport('iframe', function (options) {
if (options.async) {
var form,
iframe,
addParamChar;
return {
send: function (_, completeCallback) {
form = $('<form style="display:none;"></form>');
form.attr('accept-charset', options.formAcceptCharset);
addParamChar = /\?/.test(options.url) ? '&' : '?';
// XDomainRequest only supports GET and POST:
if (options.type === 'DELETE') {
options.url = options.url + addParamChar + '_method=DELETE';
options.type = 'POST';
} else if (options.type === 'PUT') {
options.url = options.url + addParamChar + '_method=PUT';
options.type = 'POST';
} else if (options.type === 'PATCH') {
options.url = options.url + addParamChar + '_method=PATCH';
options.type = 'POST';
}
// javascript:false as initial iframe src
// prevents warning popups on HTTPS in IE6.
// IE versions below IE8 cannot set the name property of
// elements that have already been added to the DOM,
// so we set the name along with the iframe HTML markup:
iframe = $(
'<iframe src="javascript:false;" name="iframe-transport-' +
(counter += 1) + '"></iframe>'
).bind('load', function () {
var fileInputClones,
paramNames = $.isArray(options.paramName) ?
options.paramName : [options.paramName];
iframe
.unbind('load')
.bind('load', function () {
var response;
// Wrap in a try/catch block to catch exceptions thrown
// when trying to access cross-domain iframe contents:
try {
response = iframe.contents();
// Google Chrome and Firefox do not throw an
// exception when calling iframe.contents() on
// cross-domain requests, so we unify the response:
if (!response.length || !response[0].firstChild) {
throw new Error();
}
} catch (e) {
response = undefined;
}
// The complete callback returns the
// iframe content document as response object:
completeCallback(
200,
'success',
{'iframe': response}
);
// Fix for IE endless progress bar activity bug
// (happens on form submits to iframe targets):
$('<iframe src="javascript:false;"></iframe>')
.appendTo(form);
form.remove();
});
form
.prop('target', iframe.prop('name'))
.prop('action', options.url)
.prop('method', options.type);
if (options.formData) {
$.each(options.formData, function (index, field) {
$('<input type="hidden"/>')
.prop('name', field.name)
.val(field.value)
.appendTo(form);
});
}
if (options.fileInput && options.fileInput.length &&
options.type === 'POST') {
fileInputClones = options.fileInput.clone();
// Insert a clone for each file input field:
options.fileInput.after(function (index) {
return fileInputClones[index];
});
if (options.paramName) {
options.fileInput.each(function (index) {
$(this).prop(
'name',
paramNames[index] || options.paramName
);
});
}
// Appending the file input fields to the hidden form
// removes them from their original location:
form
.append(options.fileInput)
.prop('enctype', 'multipart/form-data')
// enctype must be set as encoding for IE:
.prop('encoding', 'multipart/form-data');
}
form.submit();
// Insert the file input fields at their original location
// by replacing the clones with the originals:
if (fileInputClones && fileInputClones.length) {
options.fileInput.each(function (index, input) {
var clone = $(fileInputClones[index]);
$(input).prop('name', clone.prop('name'));
clone.replaceWith(input);
});
}
});
form.append(iframe).appendTo(document.body);
},
abort: function () {
if (iframe) {
// javascript:false as iframe src aborts the request
// and prevents warning popups on HTTPS in IE6.
// concat is used to avoid the "Script URL" JSLint error:
iframe
.unbind('load')
.prop('src', 'javascript'.concat(':false;'));
}
if (form) {
form.remove();
}
}
};
}
});
// The iframe transport returns the iframe content document as response.
// The following adds converters from iframe to text, json, html, and script:
$.ajaxSetup({
converters: {
'iframe text': function (iframe) {
return iframe && $(iframe[0].body).text();
},
'iframe json': function (iframe) {
return iframe && $.parseJSON($(iframe[0].body).text());
},
'iframe html': function (iframe) {
return iframe && $(iframe[0].body).html();
},
'iframe script': function (iframe) {
return iframe && $.globalEval($(iframe[0].body).text());
}
}
});
}));
$(function() {
$('.file-upload-widget').each(function() {
var base_widget = $(this);
if ($(base_widget).find('input[type=hidden]').val()) {
$(base_widget).find('input[type=file]').hide();
} else {
$(base_widget).find('.fileinfo').hide();
}
$(this).find('input[type=file]').fileupload({
dataType: 'json',
add: function (e, data) {
$(base_widget).find('.fileprogress .bar').css('width', '0%');
$(base_widget).find('.fileprogress').show();
$(base_widget).find('.fileinfo').hide();
var jqXHR = data.submit();
},
done: function(e, data) {
$(base_widget).find('.fileprogress').hide();
$(base_widget).find('.filename').text(data.result[0].name);
$(base_widget).find('.fileinfo').show();
$(base_widget).find('input[type=hidden]').val(data.result[0].token);
$(this).hide();
},
progress: function (e, data) {
var progress = parseInt(data.loaded / data.total * 100, 10);
$(base_widget).find('.fileprogress .bar').css('width', progress + '%');
}
});
$(this).find('a.remove').click(function() {
$(base_widget).find('input[type=hidden]').val('');
$(base_widget).find('.fileinfo').hide();
$(base_widget).find('input[type=file]').show();
return false;
});
$(this).find('a.change').click(function() {
$(base_widget).find('input[type=file]').click();
return false;
});
});
});
......@@ -8,6 +8,9 @@
<link rel="stylesheet" type="text/css" media="all" href="{{ STATIC_URL }}css/style.css"/>
<script src="{{ STATIC_URL }}js/jquery.js"></script>
<script src="{{ STATIC_URL }}js/jquery-ui.js"></script>
<script src="{{ STATIC_URL }}js/jquery.fileupload.js"></script>
<script src="{{ STATIC_URL }}js/jquery.iframe-transport.js"></script>
<script src="{{ STATIC_URL }}js/qommon.fileupload.js"></script>
<link rel="stylesheet" type="text/css" media="all" href="{{ STATIC_URL }}css/smoothness/jquery-ui-1.10.0.custom.css"/>
{% block extrascripts %}
{% endblock %}
......
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