Browse Source

Rewrote upload code to do progress bar updating from the UI via polling, rather than streaming the progress bar ticks from the server.

Still to do: Rewrite import code to do something similar, and refactor unit tests accordingly.
Dan Helfman 9 years ago
parent
commit
23b3884e83
6 changed files with 216 additions and 330 deletions
  1. 56
    96
      controller/Files.py
  2. 30
    8
      static/css/style.css
  3. 0
    58
      static/css/upload.css
  4. 130
    42
      static/js/Wiki.js
  5. 0
    98
      view/Progress_bar.py
  6. 0
    28
      view/Upload_page.py

+ 56
- 96
controller/Files.py View File

@@ -5,11 +5,12 @@ import cgi
5 5
 import time
6 6
 import urllib
7 7
 import os.path
8
+import httplib
8 9
 import tempfile
9 10
 import cherrypy
10 11
 from PIL import Image
11 12
 from cStringIO import StringIO
12
-from threading import Lock, Event
13
+from threading import Lock
13 14
 from chardet.universaldetector import UniversalDetector
14 15
 from Expose import expose
15 16
 from Validate import validate, Valid_int, Valid_bool, Validation_error
@@ -20,10 +21,9 @@ from model.File import File
20 21
 from model.User import User
21 22
 from model.Notebook import Notebook
22 23
 from model.Download_access import Download_access
23
-from view.Upload_page import Upload_page
24 24
 from view.Blank_page import Blank_page
25 25
 from view.Json import Json
26
-from view.Progress_bar import stream_progress, stream_quota_error, quota_error_script, general_error_script
26
+from view.Progress_bar import quota_error_script, general_error_script
27 27
 from view.File_preview_page import File_preview_page
28 28
 
29 29
 
@@ -87,14 +87,11 @@ class Upload_file( object ):
87 87
     self.__content_length = content_length
88 88
     self.__file_received_bytes = 0
89 89
     self.__total_received_bytes = cherrypy.request.rfile.bytes_read
90
-    self.__total_received_bytes_updated = Event()
91
-    self.__complete = Event()
92 90
   
93 91
   def write( self, data ):
94 92
     self.__file.write( data )
95 93
     self.__file_received_bytes += len( data )
96 94
     self.__total_received_bytes = cherrypy.request.rfile.bytes_read
97
-    self.__total_received_bytes_updated.set()
98 95
 
99 96
   def tell( self ):
100 97
     return self.__file.tell()
@@ -108,25 +105,13 @@ class Upload_file( object ):
108 105
 
109 106
     return self.__file.read( size )
110 107
 
111
-  def wait_for_total_received_bytes( self ):
112
-    self.__total_received_bytes_updated.wait( timeout = cherrypy.server.socket_timeout )
113
-    self.__total_received_bytes_updated.clear()
114
-    return self.__total_received_bytes
115
-
116 108
   def close( self ):
117 109
     self.__file.close()
118
-    self.complete()
119
-
120
-  def complete( self ):
121
-    self.__complete.set()
122 110
 
123 111
   def delete( self ):
124 112
     self.__file.close()
125 113
     self.delete_file( self.__file_id )
126 114
 
127
-  def wait_for_complete( self ):
128
-    self.__complete.wait( timeout = cherrypy.server.socket_timeout )
129
-
130 115
   @staticmethod
131 116
   def make_server_filename( file_id ):
132 117
     global files_dir
@@ -522,7 +507,7 @@ class Files( object ):
522 507
 
523 508
     return stream()
524 509
 
525
-  @expose( view = Upload_page )
510
+  @expose( view = Json )
526 511
   @strongly_expire
527 512
   @end_transaction
528 513
   @grab_user_id
@@ -531,10 +516,9 @@ class Files( object ):
531 516
     note_id = Valid_id(),
532 517
     user_id = Valid_id( none_okay = True ),
533 518
   )
534
-  def upload_page( self, notebook_id, note_id, user_id ):
519
+  def upload_id( self, notebook_id, note_id, user_id ):
535 520
     """
536
-    Provide the information necessary to display the file upload page, including the generation of a
537
-    unique file id.
521
+    Generate and return a unique file id for use in an upload.
538 522
 
539 523
     @type notebook_id: unicode
540 524
     @param notebook_id: id of the notebook that the upload will be to
@@ -543,7 +527,7 @@ class Files( object ):
543 527
     @type user_id: unicode or NoneType
544 528
     @param user_id: id of current logged-in user (if any)
545 529
     @rtype: unicode
546
-    @return: rendered HTML page
530
+    @return: { 'file_id': file_id }
547 531
     @raise Access_error: the current user doesn't have access to the given notebook
548 532
     """
549 533
     notebook = self.__users.load_notebook( user_id, notebook_id, read_write = True, note_id = note_id )
@@ -554,47 +538,7 @@ class Files( object ):
554 538
     file_id = self.__database.next_id( File )
555 539
 
556 540
     return dict(
557
-      notebook_id = notebook_id,
558
-      note_id = note_id,
559 541
       file_id = file_id,
560
-      label_text = u"attach file",
561
-      instructions_text = u"Please select a file to upload.",
562
-    )
563
-
564
-  @expose( view = Upload_page )
565
-  @strongly_expire
566
-  @end_transaction
567
-  @grab_user_id
568
-  @validate(
569
-    notebook_id = Valid_id(),
570
-    user_id = Valid_id( none_okay = True ),
571
-  )
572
-  def import_page( self, notebook_id, user_id ):
573
-    """
574
-    Provide the information necessary to display the file import page, including the generation of a
575
-    unique file id.
576
-
577
-    @type notebook_id: unicode
578
-    @param notebook_id: id of the notebook that the upload will be to
579
-    @type note_id: unicode
580
-    @param user_id: id of current logged-in user (if any)
581
-    @rtype: unicode
582
-    @return: rendered HTML page
583
-    @raise Access_error: the current user doesn't have access to the given notebook
584
-    """
585
-    notebook = self.__users.load_notebook( user_id, notebook_id, read_write = True )
586
-
587
-    if not notebook or notebook.read_write == Notebook.READ_WRITE_FOR_OWN_NOTES:
588
-      raise Access_error()
589
-
590
-    file_id = self.__database.next_id( File )
591
-
592
-    return dict(
593
-      notebook_id = notebook_id,
594
-      note_id = None,
595
-      file_id = file_id,
596
-      label_text = u"import file",
597
-      instructions_text = u"Please select a CSV file of notes to import into a new notebook.",
598 542
     )
599 543
 
600 544
   @expose( view = Blank_page )
@@ -652,7 +596,11 @@ class Files( object ):
652 596
     # if we didn't receive all of the expected data, abort
653 597
     if uploaded_file.total_received_bytes < uploaded_file.content_length:
654 598
       uploaded_file.delete()
655
-      return dict() # hopefully, the call to progress() will report this to the user
599
+      return dict( script = general_error_script % u"The uploaded file was not fully received. Please try again or contact support." )
600
+
601
+    if uploaded_file.file_received_bytes == 0:
602
+      uploaded_file.delete()
603
+      return dict( script = general_error_script % u"The uploaded file was not received. Please make sure that the file exists." )
656 604
 
657 605
     # if the uploaded file's size would put the user over quota, bail and inform the user
658 606
     rate_plan = self.__users.rate_plan( user.rate_plan )
@@ -671,67 +619,79 @@ class Files( object ):
671 619
 
672 620
     return dict()
673 621
 
674
-  @expose()
622
+  @expose( view = Json )
675 623
   @strongly_expire
676 624
   @end_transaction
677 625
   @grab_user_id
678 626
   @validate(
679 627
     file_id = Valid_id(),
680
-    filename = unicode,
681 628
     user_id = Valid_id( none_okay = True ),
682 629
   )
683
-  def progress( self, file_id, filename, user_id = None ):
630
+  def progress( self, file_id, user_id = None ):
684 631
     """
685
-    Stream information on a file that is in the process of being uploaded. This method does not
686
-    perform any access checks, but the only information streamed is a progress bar and upload
687
-    percentage.
632
+    Return information on a file that is in the process of being uploaded. This method does not
633
+    perform any access checks, but the only information revealed is the file's upload progress.
634
+
635
+    This method is intended to be polled while the file is uploading, and its returned data is
636
+    intended to mimic the API described here:
637
+    http://wiki.nginx.org//NginxHttpUploadProgressModule
688 638
 
689 639
     @type file_id: unicode
690 640
     @param file_id: id of a currently uploading file
691
-    @type filename: unicode
692
-    @param filename: name of the file to report on
693 641
     @type user_id: unicode or NoneType
694 642
     @param user_id: id of current logged-in user (if any)
695
-    @rtype: unicode
696
-    @return: streaming HTML progress bar
643
+    @rtype: dict
644
+    @return: one of the following:
645
+      { 'state': 'starting' }                          // file_id is unknown
646
+      { 'state': 'done' }                              // upload is complete
647
+      { 'state': 'error', 'status': http_error_code }  // upload generated an HTTP error
648
+      { 'state': 'uploading',                          // upload is in progress
649
+        'received': bytes_received, 'size': total_bytes }
697 650
     """
698 651
     global current_uploads
699 652
 
700
-    # poll until the file is uploading (as determined by current_uploads) or completely uploaded (in
701
-    # the database with a filename)
702
-    while True:
703
-      uploading_file = current_uploads.get( file_id )
704
-      db_file = None
705
-
706
-      if uploading_file:
707
-        fraction_reported = 0.0
708
-        break
709
-
710
-      db_file = self.__database.load( File, file_id )
711
-      if not db_file:
712
-        raise Upload_error( u"The file id is unknown" )
713
-      if db_file.filename is None:
714
-        time.sleep( 0.1 )
715
-        continue
716
-      fraction_reported = 1.0
717
-      break
653
+    uploading_file = current_uploads.get( file_id )
654
+    db_file = None
718 655
 
719
-    # if the uploaded file's size would put the user over quota, bail and inform the user
720 656
     if uploading_file:
657
+      # if the uploaded file's size would put the user over quota, bail and inform the user
721 658
       SOFT_QUOTA_FACTOR = 1.05 # fudge factor since content_length isn't really the file's actual size
722 659
 
723 660
       user = self.__database.load( User, user_id )
724 661
       if not user:
725
-        raise Access_error()
662
+        return dict(
663
+          state = "error",
664
+          stauts = httplib.FORBIDDEN,
665
+        )
726 666
 
727 667
       rate_plan = self.__users.rate_plan( user.rate_plan )
728 668
 
729 669
       storage_quota_bytes = rate_plan.get( u"storage_quota_bytes" )
730 670
       if storage_quota_bytes and \
731 671
          user.storage_bytes + uploading_file.content_length > storage_quota_bytes * SOFT_QUOTA_FACTOR:
732
-        return stream_quota_error()
672
+        return dict(
673
+          state = "error",
674
+          stauts = httplib.REQUEST_ENTITY_TOO_LARGE,
675
+        )
676
+
677
+      return dict(
678
+        state = u"uploading",
679
+        received = uploading_file.total_received_bytes,
680
+        size = uploading_file.content_length,
681
+      );
682
+
683
+    db_file = self.__database.load( File, file_id )
684
+    if not db_file:
685
+      return dict(
686
+        state = "error",
687
+        stauts = httplib.NOT_FOUND,
688
+      )
689
+
690
+    if db_file.filename is None:
691
+      return dict( state = u"starting" );
733 692
 
734
-    return stream_progress( uploading_file, filename, fraction_reported )
693
+    # the file is completely uploaded (in the database with a filename)
694
+    return dict( state = u"done" );
735 695
 
736 696
   @expose( view = Json )
737 697
   @strongly_expire

+ 30
- 8
static/css/style.css View File

@@ -281,6 +281,12 @@ h1 {
281 281
   background-image: url(/static/images/grabber_hover.png);
282 282
 }
283 283
 
284
+#tick_preload {
285
+  height: 0;
286
+  overflow: hidden;
287
+  background-image: url(/static/images/tick.png);
288
+}
289
+
284 290
 #note_tree_area {
285 291
   position: fixed;
286 292
   width: 20em;
@@ -886,7 +892,7 @@ h1 {
886 892
   max-height: 20em;
887 893
   min-width: 10em;
888 894
   overflow: auto;
889
-  padding: 0.5em;
895
+  padding: 0.75em;
890 896
   border: 1px solid #000000;
891 897
   background-color: #ffff99;
892 898
 }
@@ -1159,6 +1165,8 @@ h1 {
1159 1165
 }
1160 1166
 
1161 1167
 .button {
1168
+  -moz-border-radius: 3px;
1169
+  -webkit-border-radius: 3px;
1162 1170
   border-style: outset;
1163 1171
   border-width: 0px;
1164 1172
   background-color: #d0e0f0;
@@ -1183,13 +1191,6 @@ h1 {
1183 1191
   color: #ff6600;
1184 1192
 }
1185 1193
 
1186
-.upload_frame {
1187
-  padding: 0;
1188
-  margin: 0;
1189
-  width: 50em;
1190
-  height: 5em;
1191
-}
1192
-
1193 1194
 .file_thumbnail {
1194 1195
   margin-right: 0.5em;
1195 1196
   vertical-align: top;
@@ -1243,3 +1244,24 @@ h1 {
1243 1244
   padding: 0.5em;
1244 1245
   font-size: 110%;
1245 1246
 }
1247
+
1248
+#progress_row {
1249
+  margin-top: 0.75em;
1250
+}
1251
+
1252
+#progress_border {
1253
+  border: 1px solid #000000;
1254
+  background-color: #ffffff;
1255
+  width: 20em;
1256
+  height: 1em;
1257
+}
1258
+
1259
+#progress_bar {
1260
+  width: 0;
1261
+  height: 1em;
1262
+}
1263
+
1264
+#progress_percent {
1265
+  margin-left: 0.75em;
1266
+  margin-right: 0.75em;
1267
+}

+ 0
- 58
static/css/upload.css View File

@@ -1,58 +0,0 @@
1
-html, body {
2
-  padding: 0;
3
-  margin: 0;
4
-  line-height: 140%;
5
-  font-family: sans-serif;
6
-  background-color: #ffff99;
7
-}
8
-
9
-body {
10
-  font-size: 72%;
11
-}
12
-
13
-form {
14
-  margin-bottom: 0.5em;
15
-}
16
-
17
-div {
18
-  margin-bottom: 0.5em;
19
-}
20
-
21
-.field_label {
22
-  font-weight: bold;
23
-}
24
-
25
-.text_field {
26
-  border: #999999 1px solid;
27
-}
28
-
29
-.button {
30
-  border-style: outset;
31
-  border-width: 0px;
32
-  background-color: #d0e0f0;
33
-  font-size: 100%;
34
-  outline: none;
35
-  cursor: pointer;
36
-  margin-left: 0.25em;
37
-}
38
-
39
-.button:hover {
40
-  background-color: #ffcc66;
41
-}
42
-
43
-#progress_border {
44
-  border: 1px solid #000000;
45
-  background-color: #ffffff;
46
-  width: 20em;
47
-  height: 1em;
48
-}
49
-
50
-td {
51
-  vertical-align: top;
52
-}
53
-
54
-#tick_preload {
55
-  height: 0;
56
-  overflow: hidden;
57
-  background-image: url(/static/images/tick.png);
58
-}

+ 130
- 42
static/js/Wiki.js View File

@@ -3609,80 +3609,160 @@ function Upload_pulldown( wiki, notebook_id, invoker, editor, link, ephemeral )
3609 3609
   Pulldown.call( this, wiki, notebook_id, "upload_" + editor.id, this.link, editor.iframe, ephemeral );
3610 3610
   wiki.down_image_button( "attachFile" );
3611 3611
 
3612
-  var vaguely_random = new Date().getTime();
3613 3612
   this.invoker = invoker;
3614 3613
   this.editor = editor;
3615 3614
   this.iframe = createDOM( "iframe", {
3616
-    "src": "/files/upload_page?notebook_id=" + notebook_id + "&note_id=" + editor.id,
3617
-    "frameBorder": "0",
3618
-    "scrolling": "no",
3619
-    // if a new iframe has an id/name that WebKit has already seen, then it will just use its
3620
-    // previous src value and ignore our new src value here. workaround: don't use the same id!
3621
-    "id": "upload_frame_" + vaguely_random,
3622
-    "name": "upload_frame_" + vaguely_random,
3623
-    "class": "upload_frame"
3615
+    "src": "about:blank",
3616
+    "id": "upload_frame",
3617
+    "name": "upload_frame",
3618
+    "class": "upload_frame undisplayed"
3624 3619
   } );
3625 3620
   this.iframe.pulldown = this;
3621
+
3626 3622
   this.file_id = null;
3627 3623
   this.uploading = false;
3624
+  this.poller = null;
3625
+  this.POLL_INTERVAL = 500;
3628 3626
 
3629 3627
   var self = this;
3630
-  connect( this.iframe, "onload", function ( event ) { self.init_frame(); } );
3631 3628
 
3632 3629
   appendChildNodes( this.div, this.iframe );
3633 3630
 
3634
-  this.progress_iframe = createDOM( "iframe", {
3635
-    "frameBorder": "0",
3636
-    "scrolling": "no",
3637
-    "id": "progress_frame_" + vaguely_random,
3638
-    "name": "progress_frame_" + vaguely_random,
3639
-    "class": "upload_frame"
3631
+  this.upload_area = createDOM( "span" );
3632
+  this.upload_button = createDOM( "input", { "id": "upload_button", "type": "submit", "class": "button", "value": "upload" } );
3633
+  appendChildNodes( this.upload_area, createDOM( "form",
3634
+    {
3635
+      "target": "upload_frame",
3636
+      "action": "/files/upload?file_id=new",
3637
+      "method": "post",
3638
+      "enctype": "multipart/form-data",
3639
+      "id": "upload_form"
3640
+    },
3641
+    createDOM( "span", { "class": "field_label" }, "attach file: " ), // TODO: or "import file"
3642
+    createDOM( "input", { "name": "notebook_id", "id": "notebook_id", "type": "hidden", "value": notebook_id } ),
3643
+    createDOM( "input", { "name": "note_id", "id": "note_id", "type": "hidden", "value": editor ? editor.id : "" } ),
3644
+    createDOM( "input", { "name": "upload", "id": "upload", "type": "file", "class": "text_field", "size": "30" } ),
3645
+    this.upload_button
3646
+  ) );
3647
+  this.upload_button.disabled = true;
3648
+
3649
+  appendChildNodes( this.upload_area, createDOM( "p", {}, "Please select a file to upload." ) ); // TODO: or import CSV
3650
+  appendChildNodes( this.upload_area, createDOM( "span", { "id": "tick_preload" } ) );
3651
+  appendChildNodes( this.upload_area, createDOM( "input", { "name": "file_id", "id": "file_id", "type": "hidden", "value": "new" } ) );
3652
+  appendChildNodes( this.div, this.upload_area );
3653
+
3654
+  connect( this.upload_button, "onclick", function ( event ) {
3655
+    self.upload_started();
3640 3656
   } );
3641
-  addElementClass( this.progress_iframe, "undisplayed" );
3642 3657
 
3643
-  appendChildNodes( this.div, this.progress_iframe );
3658
+  // grab the next available file id
3659
+  this.invoker.invoke( "/files/upload_id", "POST",
3660
+    { "notebook_id": notebook_id, "note_id": editor ? editor.id : "" },
3661
+    function( result ) { self.update_file_id( result ); }
3662
+  );
3663
+
3644 3664
   Pulldown.prototype.finish_init.call( this );
3645 3665
 }
3646 3666
 
3647 3667
 Upload_pulldown.prototype = new function () { this.prototype = Pulldown.prototype; };
3648 3668
 Upload_pulldown.prototype.constructor = Upload_pulldown;
3649 3669
 
3650
-Upload_pulldown.prototype.init_frame = function () {
3651
-  var self = this;
3652
-  var doc = this.iframe.contentDocument || this.iframe.contentWindow.document;
3670
+Upload_pulldown.prototype.update_file_id = function ( result ) {
3671
+  this.file_id = result.file_id;
3653 3672
 
3654
-  withDocument( doc, function () {
3655
-    connect( "upload_button", "onclick", function ( event ) {
3656
-      withDocument( doc, function () {
3657
-        self.upload_started( getElement( "file_id" ).value );
3658
-      } );
3659
-    } );
3673
+  var upload_form = getElement( "upload_form" )
3674
+  if ( upload_form )
3675
+    upload_form.action = "/files/upload?file_id=" + this.file_id;
3660 3676
 
3661
-    connect( doc.body, "onmouseover", function ( event ) {
3662
-      self.ephemeral = false;
3663
-    } );
3664
-  } );
3677
+  var file_id_node = getElement( "file_id" );
3678
+  if ( file_id_node )
3679
+    file_id_node.value = this.file_id;
3680
+
3681
+  this.upload_button.disabled = false;
3665 3682
 }
3666 3683
 
3667 3684
 Upload_pulldown.prototype.upload_started = function ( file_id ) {
3668
-  this.file_id = file_id;
3669 3685
   this.uploading = true;
3670 3686
   var filename = base_upload_filename();
3671 3687
 
3672
-  // make the upload iframe invisible but still present so that the upload continues
3673
-  setElementDimensions( this.iframe, { "h": "0" } );
3674
-
3675 3688
   // if the current title is blank, replace the title with the upload's filename
3676 3689
   var title = link_title( this.link );
3677 3690
   if ( title == "" )
3678 3691
     this.link.innerHTML = filename;
3692
+  
3693
+  this.cancel_button = createDOM( "input", { "type": "submit", "id": "cancel_button", "class": "button", "value": "cancel" } );
3679 3694
 
3680
-  removeElementClass( this.progress_iframe, "undisplayed" );
3681
-  var progress_url = "/files/progress?file_id=" + file_id + "&filename=" + escape( filename );
3695
+  var progress_area = createDOM( "table", {},
3696
+    createDOM( "tr", {},
3697
+      createDOM( "td", { "class": "field_label", "colspan": "2" }, "uploading " + filename + ": " )
3698
+    ),
3699
+    createDOM( "tr", { "id": "progress_row" },
3700
+      createDOM( "td", {},
3701
+        createDOM( "div", { "id": "progress_border" },
3702
+          createDOM( "img", { "src": "/static/images/tick.png", "id": "progress_bar" } )
3703
+        )
3704
+      ),
3705
+      createDOM( "td", { "class": "progress_right" },
3706
+        createDOM( "span", { "id": "progress_percent" }, "0%" ),
3707
+        this.cancel_button
3708
+      )
3709
+    )
3710
+  );
3682 3711
 
3683
-  this.progress_iframe.src = progress_url;
3712
+  disconnectAll( this.upload_button );
3713
+  addElementClass( this.upload_area, "undisplayed" );
3714
+  appendChildNodes( this.div, progress_area );
3715
+  this.upload_button = null;
3716
+
3717
+  var self = this;
3718
+  connect( this.cancel_button, "onclick", function ( event ) {
3719
+    self.cancel_due_to_click();
3720
+  } );
3721
+
3722
+  // start polling for the upload progress
3723
+  this.poller = setTimeout( function () { self.update_progress(); }, this.POLL_INTERVAL );
3684 3724
 }
3685 3725
 
3726
+Upload_pulldown.prototype.update_progress = function () {
3727
+  var self = this;
3728
+  var BAR_WIDTH_EM = 20.0;
3729
+
3730
+  // TODO: send X- HTTP header nginx expects with file_id
3731
+  this.invoker.invoke( "/files/progress", "GET",
3732
+    { "file_id": this.file_id },
3733
+    function( result ) {
3734
+      var fraction_done = 0.0;
3735
+      if ( !self.uploading )
3736
+        return;
3737
+
3738
+      if ( result.state == "error" ) {
3739
+        if ( result.status == 413 )
3740
+          self.cancel_due_to_quota();
3741
+        else
3742
+          self.cancel_due_to_error( "An error occurred when uploading the file." );
3743
+        return;
3744
+      }
3745
+
3746
+      if ( result.state == "uploading" && result.size > 0 )
3747
+        fraction_done = Math.min( result.received / result.size, 1.0 );
3748
+      else if ( result.state == "done" )
3749
+        fraction_done = 1.0;
3750
+
3751
+      if ( fraction_done > 0.0 ) {
3752
+        var percent = fraction_done * 100.0;
3753
+        setElementDimensions( "progress_bar", { "w": fraction_done * BAR_WIDTH_EM }, "em" );
3754
+        replaceChildNodes( "progress_percent", parseInt( percent ) + "%" );
3755
+      }
3756
+
3757
+      // the brief delay gives a brief moment for the progress bar to appear at 100%
3758
+      if ( result.state == "done" )
3759
+        setTimeout( function () { self.upload_complete(); }, 1 );
3760
+      else
3761
+        this.poller = setTimeout( function () { self.update_progress(); }, self.POLL_INTERVAL );
3762
+    }
3763
+  );
3764
+};
3765
+
3686 3766
 Upload_pulldown.prototype.upload_complete = function () {
3687 3767
   if ( /MSIE/.test( navigator.userAgent ) )
3688 3768
     var quote_filename = true;
@@ -3702,6 +3782,7 @@ Upload_pulldown.prototype.update_position = function ( always_left_align ) {
3702 3782
 }
3703 3783
 
3704 3784
 Upload_pulldown.prototype.cancel_due_to_click = function () {
3785
+  // when the uploading iframe closes, that should effectively cancel the upload
3705 3786
   this.uploading = false;
3706 3787
   this.wiki.display_message( "The file upload has been cancelled." )
3707 3788
   this.shutdown();
@@ -3713,7 +3794,7 @@ Upload_pulldown.prototype.cancel_due_to_quota = function () {
3713 3794
 
3714 3795
   this.wiki.display_error(
3715 3796
     "That file is too large for your available storage space. Before uploading, please delete some notes or files, empty the trash, or",
3716
-    [ createDOM( "a", { "href": "/upgrade" }, "upgrade" ), " your account." ]
3797
+    [ createDOM( "a", { "href": "/pricing" }, "upgrade" ), " your account." ]
3717 3798
   );
3718 3799
 }
3719 3800
 
@@ -3727,11 +3808,18 @@ Upload_pulldown.prototype.shutdown = function () {
3727 3808
   if ( this.uploading )
3728 3809
     return;
3729 3810
 
3811
+  if ( this.poller )
3812
+    clearTimeout( this.poller );
3813
+
3814
+  if ( this.upload_button )
3815
+    disconnectAll( this.upload_button );
3816
+
3817
+  if ( this.cancel_button )
3818
+    disconnectAll( this.cancel_button );
3819
+
3730 3820
   // in Internet Explorer, the upload won't actually cancel without an explicit Stop command
3731
-  if ( !this.iframe.contentDocument && this.iframe.contentWindow ) {
3821
+  if ( !this.iframe.contentDocument && this.iframe.contentWindow )
3732 3822
     this.iframe.contentWindow.document.execCommand( 'Stop' );
3733
-    this.progress_iframe.contentWindow.document.execCommand( 'Stop' );
3734
-  }
3735 3823
 
3736 3824
   Pulldown.prototype.shutdown.call( this );
3737 3825
   if ( this.link )

+ 0
- 98
view/Progress_bar.py View File

@@ -2,86 +2,6 @@ import cgi
2 2
 from config.Version import VERSION
3 3
 
4 4
 
5
-def stream_progress( uploading_file, filename, fraction_reported ):
6
-  """
7
-  Stream a progress meter as a file uploads.
8
-  """
9
-  progress_bytes = 0
10
-  progress_width_em = 20
11
-  tick_increment = 0.01
12
-  progress_bar = u'<img src="/static/images/tick.png" style="width: %sem; height: 1em;" id="progress_bar" />' % \
13
-    ( progress_width_em * tick_increment )
14
-
15
-  yield \
16
-    u"""
17
-    <html>
18
-    <head>
19
-      <link href="/static/css/upload.css?%s" type="text/css" rel="stylesheet" />
20
-      <script type="text/javascript" src="/static/js/MochiKit.js?%s"></script>
21
-      <meta content="text/html; charset=UTF-8" http_equiv="content-type" />
22
-    </head>
23
-    <body>
24
-    """ % ( VERSION, VERSION )
25
-
26
-  FILENAME_TRUNCATION_WIDTH = 40
27
-  base_filename = filename.split( u"/" )[ -1 ].split( u"\\" )[ -1 ]
28
-  if len( base_filename ) > FILENAME_TRUNCATION_WIDTH:
29
-    base_filename = base_filename[ : FILENAME_TRUNCATION_WIDTH ] + u"..."
30
-
31
-  yield \
32
-    u"""
33
-    <div class="field_label">uploading %s: </div>
34
-    <table><tr>
35
-    <td><div id="progress_border">
36
-    %s
37
-    </div></td>
38
-    <td></td>
39
-    <td><span id="status">0%%</span></td>
40
-    <td></td>
41
-    <td><input type="submit" id="cancel_button" class="button" value="cancel" onclick="withDocument( window.parent.document, function () { getFirstElementByTagAndClassName( "iframe", "upload_frame" ).pulldown.cancel_due_to_click(); } );" /></td>
42
-    </tr></table>
43
-    <script type="text/javascript">
44
-    function tick( fraction ) {
45
-      setElementDimensions(
46
-        "progress_bar",
47
-        { "w": %s * fraction }, "em"
48
-      );
49
-      if ( fraction >= 1.0 )
50
-        replaceChildNodes( "status", "100%%" );
51
-      else
52
-        replaceChildNodes( "status", Math.floor( fraction * 100.0 ) + "%%" );
53
-    }
54
-    </script>
55
-    """ % ( cgi.escape( base_filename ), progress_bar, progress_width_em )
56
-
57
-  if uploading_file:
58
-    received_bytes = 0
59
-    while received_bytes < uploading_file.content_length:
60
-      received_bytes = uploading_file.wait_for_total_received_bytes()
61
-      fraction_done = float( received_bytes ) / float( uploading_file.content_length )
62
-
63
-      if fraction_done > 1.0: fraction_done = 1.0
64
-
65
-      if fraction_done == 1.0 or fraction_done > fraction_reported + tick_increment:
66
-        fraction_reported = fraction_done
67
-        yield '<script type="text/javascript">tick(%s);</script>' % fraction_reported
68
-
69
-    uploading_file.wait_for_complete()
70
-
71
-  if fraction_reported < 1.0:
72
-    yield "An error occurred when uploading the file.</body></html>"
73
-    return
74
-
75
-  yield \
76
-    u"""
77
-    <script type="text/javascript">
78
-    withDocument( window.parent.document, function () { var frame = getFirstElementByTagAndClassName( "iframe", "upload_frame" ); if ( frame && frame.pulldown ) frame.pulldown.upload_complete(); } );
79
-    </script>
80
-    </body>
81
-    </html>
82
-    """
83
-
84
-
85 5
 general_error_script = \
86 6
   """
87 7
   withDocument( window.parent.document, function () { var frame = getFirstElementByTagAndClassName( "iframe", "upload_frame" ); if ( frame && frame.pulldown ) frame.pulldown.cancel_due_to_error( "%s" ); } );
@@ -92,21 +12,3 @@ quota_error_script = \
92 12
   """
93 13
   withDocument( window.parent.document, function () { var frame = getFirstElementByTagAndClassName( "iframe", "upload_frame" ); if ( frame && frame.pulldown ) frame.pulldown.cancel_due_to_quota(); } );
94 14
   """
95
-
96
-
97
-def stream_quota_error():
98
-  yield \
99
-    u"""
100
-    <html>
101
-    <head>
102
-      <link href="/static/css/upload.css?%s" type="text/css" rel="stylesheet" />
103
-      <script type="text/javascript" src="/static/js/MochiKit.js?%s"></script>
104
-      <meta content="text/html; charset=UTF-8" http_equiv="content-type" />
105
-    </head>
106
-    <body>
107
-    <script type="text/javascript">
108
-    %s
109
-    </script>
110
-    </body>
111
-    </html>
112
-    """ % ( VERSION, VERSION, quota_error_script )

+ 0
- 28
view/Upload_page.py View File

@@ -1,28 +0,0 @@
1
-from Tags import Html, Head, Link, Meta, Body, P, Form, Span, Input
2
-from config.Version import VERSION
3
-
4
-
5
-class Upload_page( Html ):
6
-  def __init__( self, notebook_id, note_id, file_id, label_text, instructions_text ):
7
-    Html.__init__(
8
-      self,
9
-      Head(
10
-        Link( href = u"/static/css/upload.css?%s" % VERSION, type = u"text/css", rel = u"stylesheet" ),
11
-        Meta( content = u"text/html; charset=UTF-8", http_equiv = u"content-type" ),
12
-      ),
13
-      Body(
14
-        Form(
15
-          Span( u"%s: " % label_text, class_ = u"field_label" ),
16
-          Input( type = u"hidden", id = u"notebook_id", name = u"notebook_id", value = notebook_id ),
17
-          Input( type = u"hidden", id = u"note_id", name = u"note_id", value = note_id or u"" ),
18
-          Input( type = u"file", id = u"upload", name = u"upload", class_ = "text_field", size = u"30" ),
19
-          Input( type = u"submit", id = u"upload_button", class_ = u"button", value = u"upload" ),
20
-          action = u"/files/upload?file_id=%s" % file_id,
21
-          method = u"post",
22
-          enctype = u"multipart/form-data",
23
-        ),
24
-        P( instructions_text ),
25
-        Span( id = u"tick_preload" ),
26
-        Input( type = u"hidden", id = u"file_id", value = file_id ),
27
-      ),
28
-    )