from karmaworld.apps.courses.models import School
from karmaworld.apps.notes.models import Note
from karmaworld.apps.users.models import CourseKarmaEvent
+from karmaworld.apps.notes.forms import FileUploadForm
from karmaworld.utils import ajax_increment, format_session_increment_field
FLAG_FIELD = 'flags'
# Include "Add Note" button in header
kwargs['display_add_note'] = True
+ # For the Filepicker Partial template
+ kwargs['file_upload_form'] = FileUploadForm()
+
if self.request.session.get(format_session_increment_field(Course, self.object.id, FLAG_FIELD), False):
kwargs['already_flagged'] = True
from django.forms import ModelForm
from django.forms import TextInput
+from django_filepicker.forms import FPFileField
+from django_filepicker.widgets import FPFileWidget
+
from karmaworld.apps.notes.models import Note
class NoteForm(ModelForm):
widgets = {
'name': TextInput()
}
+
+class FileUploadForm(ModelForm):
+ auto_id = False
+ class Meta:
+ model = Note
+ fields = ('fp_file',)
+ widgets = {
+ 'fp_file': FPFileWidget(attrs={
+ 'id': 'filepicker-file-upload',
+ }),
+ }
note.save()
NoteKarmaEvent.create_event(mapping.user, note, NoteKarmaEvent.UPLOAD)
except (ObjectDoesNotExist, MultipleObjectsReturned):
- logger.info("Zero or multiple mappings found with fp_file " + raw_document.fp_file.url)
+ logger.info("Zero or multiple mappings found with fp_file " + raw_document.fp_file.name)
# Finally, save whatever data we got back from google
note.save()
import logging
from allauth.account.signals import user_logged_in
from django.contrib.auth.models import User
+from django.utils.safestring import mark_safe
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.core.files.storage import default_storage
from django.db.models import SET_NULL
from django.db.models.signals import post_save, post_delete, pre_save
from django.dispatch import receiver
from karmaworld.apps.users.models import NoteKarmaEvent, GenericKarmaEvent
+from karmaworld.secret.filepicker import FILEPICKER_API_KEY
+from karmaworld.utils.filepicker import encode_fp_policy, sign_fp_policy
import os
+import time
import urllib
from django.conf import settings
# https://github.com/FinalsClub/karmaworld/issues/273#issuecomment-32572169
all_read_xml_acl = '<?xml version="1.0" encoding="UTF-8"?>\n<AccessControlPolicy xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><Owner><ID>710efc05767903a0eae5064bbc541f1c8e68f8f344fa809dc92682146b401d9c</ID><DisplayName>Andrew</DisplayName></Owner><AccessControlList><Grant><Grantee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="CanonicalUser"><ID>710efc05767903a0eae5064bbc541f1c8e68f8f344fa809dc92682146b401d9c</ID><DisplayName>Andrew</DisplayName></Grantee><Permission>READ</Permission></Grant><Grant><Grantee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="CanonicalUser"><ID>710efc05767903a0eae5064bbc541f1c8e68f8f344fa809dc92682146b401d9c</ID><DisplayName>Andrew</DisplayName></Grantee><Permission>WRITE</Permission></Grant><Grant><Grantee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="CanonicalUser"><ID>710efc05767903a0eae5064bbc541f1c8e68f8f344fa809dc92682146b401d9c</ID><DisplayName>Andrew</DisplayName></Grantee><Permission>READ_ACP</Permission></Grant><Grant><Grantee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="CanonicalUser"><ID>710efc05767903a0eae5064bbc541f1c8e68f8f344fa809dc92682146b401d9c</ID><DisplayName>Andrew</DisplayName></Grantee><Permission>WRITE_ACP</Permission></Grant><Grant><Grantee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Group"><URI>http://acs.amazonaws.com/groups/global/AllUsers</URI></Grantee><Permission>READ</Permission></Grant></AccessControlList></AccessControlPolicy>'
-def _choose_upload_to(instance, filename):
- # /school/course/year/month/day
- return u"{school}/{course}/{year}/{month}/{day}".format(
- school=instance.course.school.slug,
- course=instance.course.slug,
- year=instance.uploaded_at.year,
- month=instance.uploaded_at.month,
- day=instance.uploaded_at.day)
-
class Document(models.Model):
"""
# WARNING: This may throw an error on migration
is_hidden = models.BooleanField(default=False)
+ ###
+ # Everything Filepicker, now in one small area
+
+ # Allow pick (choose files), store (upload to S3), read (from FP repo),
+ # stat (status of FP repo files) for 1 year (current time + 365 * 24 * 3600
+ # seconds). Generated one time, at class definition upon import. So the
+ # server will need to be rebooted at least one time each year or this will
+ # go stale.
+ fp_policy_json = '{{"expiry": {0}, "call": ["pick","store","read","stat"]}}'
+ fp_policy_json = fp_policy_json.format(int(time.time() + 31536000))
+ fp_policy = encode_fp_policy(fp_policy_json)
+ fp_signature = sign_fp_policy(fp_policy)
+
+ # Hack because mimetypes conflict with extensions, but there is no way to
+ # disable mimetypes.
+ # https://github.com/Ink/django-filepicker/issues/22
+ django_filepicker.forms.FPFieldMixin.default_mimetypes = ''
+ # Now let django-filepicker do the heavy lifting. Sort of. Look at all those
+ # parameters!
fp_file = django_filepicker.models.FPFileField(
- upload_to=_choose_upload_to,
- storage=fs,
- null=True, blank=True,
- help_text=u"An uploaded file reference from Filepicker.io")
+ # FPFileField settings
+ apikey=FILEPICKER_API_KEY,
+ services='COMPUTER,DROPBOX,URL,GOOGLE_DRIVE,EVERNOTE,GMAIL,BOX,FACEBOOK,FLICKR,PICASA,IMAGE_SEARCH,WEBCAM,FTP',
+ additional_params={
+ 'data-fp-multiple': 'true',
+ 'data-fp-folders': 'true',
+ 'data-fp-button-class':
+ 'add-note-btn small-10 columns large-4',
+ 'data-fp-button-text':
+ mark_safe("<i class='fa fa-arrow-circle-o-up'></i> add notes"),
+ 'data-fp-drag-class':
+ 'dragdrop show-for-medium-up large-7 columns',
+ 'data-fp-drag-text': 'Drop Some Knowledge',
+ 'data-fp-extensions':
+ '.pdf,.doc,.docx,.txt,.html,.rtf,.odt,.png,.jpg,.jpeg,.ppt,.pptx',
+ 'data-fp-store-location': 'S3',
+ 'data-fp-policy': fp_policy,
+ 'data-fp-signature': fp_signature,
+ 'onchange': "got_file(event)",
+ },
+ # FileField settings
+ null=True, blank=True,
+ upload_to='nil', # field ignored because S3, but required.
+ verbose_name='', # prevent a label from showing up
+ )
mimetype = models.CharField(max_length=255, blank=True, null=True)
class Meta:
def get_file(self):
""" Downloads the file from filepicker.io and returns a
Django File wrapper object """
- # clean up any old downloads that are still hanging around
- if hasattr(self, 'tempfile'):
- self.tempfile.close()
- delattr(self, 'tempfile')
-
- if hasattr(self, 'filename'):
- # the file might have been moved in the meantime so
- # check first
- if os.path.exists(self.filename):
- os.remove(self.filename)
- delattr(self, 'filename')
-
- # The temporary file will be created in a directory set by the
- # environment (TEMP_DIR, TEMP or TMP)
- self.filename, header = urllib.urlretrieve(self.fp_file.name)
- name = os.path.basename(self.filename)
- disposition = header.get('Content-Disposition')
- if disposition:
- name = disposition.rpartition("filename=")[2].strip('" ')
- filename = header.get('X-File-Name')
- if filename:
- name = filename
-
- self.tempfile = open(self.filename, 'r')
- return File(self.tempfile, name=name)
+ fpf = django_filepicker.utils.FilepickerFile(self.fp_file.name)
+ fd = fpf.get_file(self.fp_file.field.additional_params)
+ # temporary workaround. FilepickerFile closes/deletes the fd when
+ # garbage collected, so write it to another fd.
+ # https://github.com/Ink/django-filepicker/issues/25
+ newfd = os.tmpfile()
+ buff_size = 1048576 # read 1 MiB at a time
+ buff = fd.read(buff_size)
+ while buff != '':
+ # write until EOF
+ newfd.write(buff)
+ buff = fd.read(buff_size)
+ # replace the Django File's python file with the reset temp file.
+ newfd.seek(0)
+ fd.file = newfd
+ # force FilepickerFile to be garbage collected now, why not
+ del fpf
+ return fd
def save(self, *args, **kwargs):
if self.name and not self.slug:
DO NOT check filepicker.py into source control.
"""
FILEPICKER_API_KEY = 0
+SECRET = 0
{% load url from future %}
{% load socialaccount %}
+{{ file_upload_form.media }}
<section id=filepicker-form class="extend-form">
-<!-- Javascript -->
-<script type="text/javascript" src="//api.filepicker.io/v1/filepicker.js"></script>
<div class="row" id="filepicker_row">
<div class="small-8 small-offset-1 columns large-10">
- <input id="filepicker-file-upload"
- type="filepicker-dragdrop"
- data-fp-apikey="A5pg98pDjQk6k3lBZ8VDVz"
- data-fp-multiple="true"
- data-fp-folders="true"
- data-fp-button-class="add-note-btn small-10 columns large-4"
- data-fp-button-text="<i class='fa fa-arrow-circle-o-up'></i> add notes"
- data-fp-drag-text="Drop Some Knowledge"
- data-fp-drag-class="dragdrop show-for-medium-up large-7 columns"
- data-fp-extensions=".pdf,.doc,.docx,.txt,.html,.rtf,.odt,.png,.jpg,.jpeg,.ppt,.pptx"
- data-fp-store-path="{{ course.school.slug }}/ {{ course.slug }}/"
- data-fp-store-location="S3"
- data-fp-services="COMPUTER,DROPBOX,URL,GOOGLE_DRIVE,EVERNOTE,GMAIL,BOX,FACEBOOK,FLICKR,PICASA,IMAGE_SEARCH,WEBCAM,FTP"
- />
+ {{ file_upload_form.as_p }}
</div>
</div> <!--.row -->
<div class="row" >
<script>
var uploaded_files = new Array();
- $(function(){
- // these are obsolete without the drag-drop widget that we removed from the partial above
- // var $dropzone = $('#filepicker_dropzone');
- var $dropzone_result = $('#filepicker_dropzone_result');
+ // these are obsolete without the drag-drop widget that we removed from the partial above
+ // var $dropzone = $('#filepicker_dropzone');
+ var $dropzone_result = $('#filepicker_dropzone_result');
- /*
- * load the file form template and append it to the form container
- * takes a fileupload event
- */
- makeFileForm = function(upFile) {
- var _form = document.getElementById('form-template').cloneNode(deep=true);
- // save the Filename to the form name field
- $(_form.children[0].children[0].children[1]).val(upFile.filename); // replace with upFile name
- _form.style.display = "inline";
- _form.id = null; // clear the unique id
- // save the FP url to the form
- $(_form.children[0].children[3].children[0]).val(upFile.url);
- // save the mimetype to the form
- $(_form.children[0].children[3].children[1]).val(upFile.mimetype);
+ /*
+ * load the file form template and append it to the form container
+ * takes a fileupload event
+ */
+ var makeFileForm = function(upFile) {
+ var _form = document.getElementById('form-template').cloneNode(deep=true);
+ // save the Filename to the form name field
+ $(_form.children[0].children[0].children[1]).val(upFile.filename); // replace with upFile name
+ _form.style.display = "inline";
+ _form.id = null; // clear the unique id
+ // save the FP url to the form
+ $(_form.children[0].children[3].children[0]).val(upFile.url);
+ // save the mimetype to the form
+ $(_form.children[0].children[3].children[1]).val(upFile.mimetype);
- document.getElementById('forms_container').appendChild(_form);
+ document.getElementById('forms_container').appendChild(_form);
- if (document.location.host === 'www.karmanotes.org' ||
- document.location.host === 'karmanotes.org') {
- _gat._getTracker()._trackEvent('upload', 'filepicker file drop');
- }
+ if (document.location.host === 'www.karmanotes.org' ||
+ document.location.host === 'karmanotes.org') {
+ _gat._getTracker()._trackEvent('upload', 'filepicker file drop');
+ }
- $('.remove').on('click', function(e){
- e.stopPropagation();
- $(this).parent().parent().remove();
- });
+ $('.remove').on('click', function(e){
+ e.stopPropagation();
+ $(this).parent().parent().remove();
+ });
- $('#save-btn').show();
- }
+ $('#save-btn').show();
+ };
- $('#save-btn').on('click', function(e){
- e.stopPropagation();
- $(this).unbind('click');
- $(this).addClass('disabled');
+ $('#save-btn').on('click', function(e){
+ e.stopPropagation();
+ $(this).unbind('click');
+ $(this).addClass('disabled');
- var saveIcon = $('#save-btn-icon');
- saveIcon.removeClass('fa-save');
- saveIcon.addClass('fa-spinner fa-spin');
+ var saveIcon = $('#save-btn-icon');
+ saveIcon.removeClass('fa-save');
+ saveIcon.addClass('fa-spinner fa-spin');
- $('#forms_container .inline-form').each(function(i,el){
- var name, tags, fpurl, course;
- name = $(el).find('.intext').val();
- fp_file = $(el).find('.fpurl').val();
- tags = $(el).find('.taggit-tags').val();
- course = $(el).find('.course_id').val();
- csrf = $(el).find('.csrf').val();
- email = $('#id_email').val();
- mimetype = $(el).find('.mimetype').val();
+ $('#forms_container .inline-form').each(function(i,el){
+ var name, tags, fpurl, course;
+ name = $(el).find('.intext').val();
+ fp_file = $(el).find('.fpurl').val();
+ tags = $(el).find('.taggit-tags').val();
+ course = $(el).find('.course_id').val();
+ csrf = $(el).find('.csrf').val();
+ email = $('#id_email').val();
+ mimetype = $(el).find('.mimetype').val();
- $.post('{% url 'upload_post' %}', {
- 'name': name,
- 'fp_file': fp_file,
- 'tags': tags,
- 'course': course,
- 'csrfmiddlewaretoken': csrf,
- 'mimetype': mimetype,
- 'email': email
- }, function(data){
- if (data === 'success') {
- // For multiple uploads, we may end up clearing and re-
- // writing this multiple times, but show the same list
- // each time.
- $('#uploaded_files').empty();
- for (var i=0; i < uploaded_files.length; i++) {
- $('#uploaded_files').append($('<li>', {text: uploaded_files[i]}));
- }
- $('#thank-points').html(uploaded_files.length*5);
- $('#success').show();
- $('#save-btn').hide();
- $('#filepicker_row').hide();
- $('#forms_container .inline-form').remove();
- $('#forms_container').hide();
- if (document.location.host === 'www.karmanotes.org' ||
- document.location.host === 'karmanotes.org') {
- _gat._getTracker()._trackEvent('upload', 'upload form submitted');
- }
- {% if user.is_authenticated %}
- setTimeout(function(){
- location.reload(true);
- }, 20000);
- {% endif %}
+ $.post('{% url 'upload_post' %}', {
+ 'name': name,
+ 'fp_file': fp_file,
+ 'tags': tags,
+ 'course': course,
+ 'csrfmiddlewaretoken': csrf,
+ 'mimetype': mimetype,
+ 'email': email
+ }, function(data){
+ if (data === 'success') {
+ // For multiple uploads, we may end up clearing and re-
+ // writing this multiple times, but show the same list
+ // each time.
+ $('#uploaded_files').empty();
+ for (var i=0; i < uploaded_files.length; i++) {
+ $('#uploaded_files').append($('<li>', {text: uploaded_files[i]}));
}
- });
- // Add the name we've just uploaded to the list
- uploaded_files.push(name);
- });
+ $('#thank-points').html(uploaded_files.length*5);
+ $('#success').show();
+ $('#save-btn').hide();
+ $('#filepicker_row').hide();
+ $('#forms_container .inline-form').remove();
+ $('#forms_container').hide();
+ if (document.location.host === 'www.karmanotes.org' ||
+ document.location.host === 'karmanotes.org') {
+ _gat._getTracker()._trackEvent('upload', 'upload form submitted');
+ }
+ {% if user.is_authenticated %}
+ setTimeout(function(){
+ location.reload(true);
+ }, 20000);
+ {% endif %}
+ }
+ });
+ // Add the name we've just uploaded to the list
+ uploaded_files.push(name);
});
-
- var fileup = document.getElementById('filepicker-file-upload');
- fileup.onchange = function(event){
- $dropzone_result.text(event);
- for (var i=0; i < event.fpfiles.length; i++){
- makeFileForm(event.fpfiles[i]);
- }
- };
-
});
+
+ var got_file = function(event){
+ $dropzone_result.text(event);
+ for (var i=0; i < event.fpfiles.length; i++){
+ makeFileForm(event.fpfiles[i]);
+ }
+ };
</script>
--- /dev/null
+import hmac
+import json
+import base64
+
+from hashlib import sha256
+from karmaworld.secret.filepicker import SECRET
+
+def encode_fp_policy(policy):
+ """ Return URL-safe Base64 encoded JSON Filepicker policy. """
+ # Validate the JSON before trying to sign it and send it off to FP. It'll
+ # be easier to trap exceptions in Python than read errors out of FP.
+
+ # drop an exception bomb if the policy is not valid JSON.
+ pypolicy = json.loads(policy)
+ # ensure expiry is included. drop excepbomb if it isn't.
+ pypolicy['expiry']
+
+ # https://developers.inkfilepicker.com/docs/security/#signPolicy
+ # encode and return
+ return base64.urlsafe_b64encode(policy)
+
+def sign_fp_policy(policy):
+ """ Return a signature appropriate for the given encoded policy. """
+ # https://developers.inkfilepicker.com/docs/security/#signPolicy
+ # hash it up, bra!
+ engine = hmac.new(SECRET, digestmod=sha256)
+ engine.update(policy)
+ return engine.hexdigest()
google-api-python-client==1.0
django-grappelli==2.4.8
git+https://github.com/FinalsClub/django-taggit.git
-django-filepicker==0.1.5
+git+https://github.com/btbonval/django-filepicker
filemagic==1.6
requests
beautifulsoup4