From 5c4c0b03d6b40de91b800753c5da375fbc5bfc69 Mon Sep 17 00:00:00 2001
From: TJ <tj@tjworld.net>
Date: Sun, 20 Jun 2010 23:43:56 +0100
Subject: [PATCH] Multi-file Attachment Facility

---
 trac/attachment.py             |   38 +++++++++++++++++++-------
 trac/htdocs/js/trac.js         |   33 +++++++++++++++++++++++
 trac/templates/attachment.html |   57 ++++++++++++++++++++++++++++++---------
 3 files changed, 105 insertions(+), 23 deletions(-)

diff --git a/trac/attachment.py b/trac/attachment.py
index 8002c16..d042390 100644
--- a/trac/attachment.py
+++ b/trac/attachment.py
@@ -606,7 +606,7 @@ class AttachmentModule(Component):
 
     # Internal methods
 
-    def _do_save(self, req, attachment):
+    def _do_save_one(self, req, attachment, upload, description):
         req.perm(attachment.resource).require('ATTACHMENT_CREATE')
         parent_resource = attachment.resource.parent
         if not resource_exists(self.env, parent_resource):
@@ -614,12 +614,8 @@ class AttachmentModule(Component):
                 _("%(parent)s doesn't exist, can't create attachment",
                   parent=get_resource_name(self.env, parent_resource)))
 
-        if 'cancel' in req.args:
-            req.redirect(get_resource_url(self.env, parent_resource, req.href))
-
-        upload = req.args['attachment']
         if not hasattr(upload, 'filename') or not upload.filename:
-            raise TracError(_('No file uploaded'))
+            raise TracError(_('No file uploaded (no filename attribute in request)'))
         if hasattr(upload.file, 'fileno'):
             size = os.fstat(upload.file.fileno())[6]
         else:
@@ -642,10 +638,10 @@ class AttachmentModule(Component):
         filename = filename.replace('\\', '/').replace(':', '/')
         filename = os.path.basename(filename)
         if not filename:
-            raise TracError(_('No file uploaded'))
+            raise TracError(_('No file uploaded (cannot normalize filename)'))
         # Now the filename is known, update the attachment resource
         # attachment.filename = filename
-        attachment.description = req.args.get('description', '')
+        attachment.description = description
         attachment.author = get_reporter_id(req, 'author')
         attachment.ipnr = req.remote_addr
 
@@ -683,8 +679,30 @@ class AttachmentModule(Component):
             attachment.filename = None
         attachment.insert(filename, upload.file, size)
 
-        req.redirect(get_resource_url(self.env, attachment.resource(id=None),
-                                      req.href))
+    def _do_save_many(self, req, attachment):
+        curr_attach = 0
+        while curr_attach < int(req.args['multiple_attachments']):
+            upload=req.args['attachment-%d' % curr_attach]
+            if hasattr(upload, 'filename') and upload.filename:
+                attachment.filename = None
+            if hasattr(upload, 'filename') and upload.filename and len(upload.filename) > 0:
+                self._do_save_one(req, attachment, upload, req.args.get('description-%d' % curr_attach, ''))
+
+            curr_attach=curr_attach+1
+
+    def _do_save(self, req, attachment):
+        req.perm(attachment.resource).require('ATTACHMENT_CREATE')
+
+        if 'cancel' in req.args:
+            req.redirect(get_resource_url(self.env, attachment.resource.parent, req.href))
+
+        if 'multiple_attachments' in req.args:
+            self._do_save_many(req, attachment)
+            req.redirect(get_resource_url(self.env, attachment.resource(id=None), req.href))
+        else:
+            self._do_save_one(req, attachment, req.args['attachment'], req.args.get('description', ''))
+            # Redirect the user to list of attachments (must add a trailing '/')
+            req.redirect(get_resource_url(self.env, attachment.resource(id=None), req.href))
 
     def _do_delete(self, req, attachment):
         req.perm(attachment.resource).require('ATTACHMENT_DELETE')
diff --git a/trac/htdocs/js/trac.js b/trac/htdocs/js/trac.js
index 9c3c717..f842fa9 100644
--- a/trac/htdocs/js/trac.js
+++ b/trac/htdocs/js/trac.js
@@ -131,3 +131,36 @@
   }
 
 })(jQuery);
+
+var ATTACHFILE_COUNTER=0;
+function manageMultipleAttachFields(_obj){
+    if (_obj.value == '') {
+        if($("#multiAttach_tbody").get(0).rows.length == 1) return;
+      $("#multiAttach_tbody").get(0).deleteRow($(_obj).attr("attachnum")*1);
+      ATTACHFILE_COUNTER=0;
+      $("#multiAttach_tbody tr").each(function(index, element){
+          $("input", $("td", element).get(0))
+              .attr("attachnum",ATTACHFILE_COUNTER)
+              .get(0).name = "attachment-"+ATTACHFILE_COUNTER;
+          $("input", $("td", element).get(1))
+              .get(0).name = "description-"+ATTACHFILE_COUNTER;
+          ATTACHFILE_COUNTER++;
+      });
+      $("#multiAttach_count").get(0).value = $("#multiAttach_count").val()*1-1;
+    } else {
+      if ($(_obj).attr("attachnum") != $("#multiAttach_count").val()-1) return;
+      var tr = $("#multiAttach_tbody").get(0).insertRow(-1);
+      var td = tr.insertCell(-1);
+      $('<input type="file" onchange="manageMultipleAttachFields(this)">')
+         .attr("attachnum",$("#multiAttach_count").val())
+         .appendTo(td)
+         .get(0).name = "attachment-"+$("#multiAttach_count").val();
+
+      td = tr.insertCell(-1);
+      $('<input type="text">')
+          .attr("size",60)
+          .appendTo(td)
+          .get(0).name = "description-"+$("#multiAttach_count").val();
+      $("#multiAttach_count").get(0).value = $("#multiAttach_count").val()*1+1;
+    }
+}
diff --git a/trac/templates/attachment.html b/trac/templates/attachment.html
index 55cb544..38d19d1 100644
--- a/trac/templates/attachment.html
+++ b/trac/templates/attachment.html
@@ -24,15 +24,43 @@
   </head>
 
   <body>
-    <div py:choose="mode" id="content" class="attachment">
-      <py:when test="'new'">
-        <h1 i18n:msg="parent">Add Attachment to <a href="${url_of(parent)}">${name_of(parent)}</a></h1>
-        <form id="attachment" method="post" enctype="multipart/form-data" action="">
+    <py:def function="attach_oneFile()">
+      <div class="field">
+        <label>File:<br /><input type="file" name="attachment" /></label>
+      </div>
+      <fieldset>
+        <legend>Attachment Info</legend>
+        <py:if test="authname == 'anonymous'">
           <div class="field">
-            <label>File<py:if test="max_size >= 0">
-              <i18n:msg params="value">(size limit ${pretty_size(max_size)})</i18n:msg></py:if>:<br />
-              <input type="file" name="attachment" /></label>
+            <label>Your email or username:<br />
+              <input type="text" name="author" size="30" value="$author" />
+            </label>
           </div>
+        </py:if>
+        <div class="field">
+          <label>Description of the file (optional):<br />
+            <input type="text" name="description" size="60" /></label>
+        </div>
+        <br />
+        <div class="options">
+          <label><input type="checkbox" name="replace" />
+            Replace existing attachment of the same name</label>
+        </div>
+        <br />
+      </fieldset>
+    </py:def>
+    <py:def function="attach_multiFile()">
+     <table><thead>
+          <tr><th align="left">File</th><th align="left">Description</th></tr>
+     </thead>
+    <tbody id="multiAttach_tbody">
+        <tr>
+            <td><input type="file" attachnum="0" name="attachment-0" onchange="manageMultipleAttachFields(this);"/></td>
+            <td><input type="text" name="description-0" size="60" /></td>
+        </tr>
+        </tbody></table>
+        <input type="hidden" id="multiAttach_count" name="multiple_attachments" value="1" />
+
           <fieldset>
             <legend>Attachment Info</legend>
             <py:if test="authname == 'anonymous'">
@@ -42,19 +70,22 @@
                 </label>
               </div>
               </py:if>
-            <div class="field">
-              <label>Description of the file (optional):<br />
-                <input type="text" name="description" size="60" /></label>
-            </div>
-            <br />
             <py:if test="authname and authname != 'anonymous'">
               <div class="options">
                 <label><input type="checkbox" name="replace" />
-                  Replace existing attachment of the same name</label>
+                  Replace existing attachments of the same name</label>
               </div>
               <br />
             </py:if>
           </fieldset>
+    </py:def>
+
+    <div py:choose="mode" id="content" class="attachment">
+      <py:when test="'new'">
+        <h1>Add Attachment to <a href="${url_of(parent)}">${name_of(parent)}</a></h1>
+        <form id="attachment" method="post" enctype="multipart/form-data" action="">
+        
+        ${attach_multiFile()}
           <div class="buttons">
             <input type="hidden" name="action" value="new" />
             <input type="hidden" name="realm" value="$parent.realm" />
-- 
1.7.0.4


