refactoring Filepicker usage, closes #308
authorBryan <btbonval@gmail.com>
Sat, 1 Feb 2014 02:08:43 +0000 (21:08 -0500)
committerBryan <btbonval@gmail.com>
Sat, 1 Feb 2014 02:08:43 +0000 (21:08 -0500)
karmaworld/apps/courses/views.py
karmaworld/apps/notes/forms.py
karmaworld/apps/notes/gdrive.py
karmaworld/apps/notes/models.py
karmaworld/secret/filepicker.py.example
karmaworld/templates/partial/filepicker.html
karmaworld/utils/filepicker.py [new file with mode: 0644]
reqs/common.txt

index bb12926f020db67297dd48f6aa88c8cc85db44b9..d485ea319fd5b1f9737fe72bdf906b8d762d8231 100644 (file)
@@ -20,6 +20,7 @@ from karmaworld.apps.courses.models import Course
 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'
@@ -76,6 +77,9 @@ class CourseDetailView(DetailView):
         # 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
 
index 1207b02baf4cfb3ba5874d2cfdb018adaa19eef4..0759ffb285f4f8fefb205020802cc84e5b6de96c 100644 (file)
@@ -5,6 +5,9 @@
 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):
@@ -14,3 +17,14 @@ 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',
+                     }),
+        }
index 24af3d0dfdb7fe787607667aa1d9e4b792bc1cb1..28701334769cde6d81e5d8158204f99fd8a10d6a 100644 (file)
@@ -251,7 +251,7 @@ def convert_raw_document(raw_document, user=None):
             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()
index 23007cf0ed484c8c5d6707f3d41b770c323b5e5f..5cc50971263ddcfeddbb7f1a295cc4676c28d4a7 100644 (file)
@@ -11,13 +11,17 @@ import traceback
 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
@@ -49,15 +53,6 @@ s3_upload_headers = {
 # 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):
     """
@@ -85,11 +80,51 @@ 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:
@@ -110,31 +145,24 @@ class Document(models.Model):
     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:
index bab7b5007acce7177935d8b290adf5340bc7b2c9..4369a289d13381a42b1e540a349cf0ba63dd4c65 100644 (file)
@@ -7,3 +7,4 @@ that file.
 DO NOT check filepicker.py into source control.
 """
 FILEPICKER_API_KEY = 0
+SECRET = 0
index 2a133ad26dee6e46229659830cb5ff58a7f0ace9..d00e2f8ab3de63f13916f9ed19a0a10fb90b75f9 100644 (file)
@@ -1,24 +1,10 @@
 {% 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>
 
 
diff --git a/karmaworld/utils/filepicker.py b/karmaworld/utils/filepicker.py
new file mode 100644 (file)
index 0000000..f816c76
--- /dev/null
@@ -0,0 +1,28 @@
+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()
index dc5f5533266e945d43f49905735024bede405bc1..f2715e76da7a24d57b364f8c44be67ab3357b6ff 100644 (file)
@@ -9,7 +9,7 @@ urllib3==1.5
 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