Implement bulk uploads for organization tasks.

We add publish and unpublish buttons to the organization tasks list
for org admins and use the POST data that we receive upon clicking
those buttons to process it and publish or unpublish the task.

Note, that list_config.addPostButton call also enables the per-row
checkboxes on the list.
diff --git a/app/soc/modules/gci/views/dashboard.py b/app/soc/modules/gci/views/dashboard.py
index c9394c1..071bcc7 100644
--- a/app/soc/modules/gci/views/dashboard.py
+++ b/app/soc/modules/gci/views/dashboard.py
@@ -17,8 +17,11 @@
 """Module for the GCI participant dashboard.
 """
 
+import logging
+
 from google.appengine.ext import db
 
+from django.utils import simplejson
 from django.utils.dateformat import format
 from django.utils.translation import ugettext
 
@@ -661,6 +664,24 @@
         lambda e, *args: data.redirect.id(e.key().id()).
             urlOf('gci_view_task'))
 
+    # Add publish/unpublish buttons to the list and enable per-row checkboxes.
+    #
+    # It is very important to note that the setRowAction should go before
+    # addPostButton call for the checkbox to be present on the list.
+    # setRowAction sets multiselect attribute to False which is set to True
+    # by addPostButton method and should be True for the checkbox to be
+    # present on the list.
+    if data.is_org_admin:
+      # publish/unpublish tasks
+      bounds = [1,'all']
+      # GCITask is keyed based solely on the entity ID, because it is very
+      # difficult to group it with either organizations or profiles, so to
+      # make the querying easier across entity groups we only use entity ids
+      # as keys.
+      keys = ['key']
+      list_config.addPostButton('publish', 'Publish', '', bounds, keys)
+      list_config.addPostButton('unpublish', 'Unpublish', '', bounds, keys)
+
     self._list_config = list_config
 
   def templatePath(self):
@@ -668,6 +689,68 @@
     """
     return'v2/modules/gci/dashboard/list_component.html'
 
+  def post(self):
+    """Processes the form post data by checking what buttons were pressed.
+    """
+    idx = lists.getListIndex(self.request)
+    if idx != self.IDX:
+      return None
+
+    data = self.data.POST.get('data')
+
+    if not data:
+      raise exceptions.BadRequest('Missing data')
+
+    parsed = simplejson.loads(data)
+
+    button_id = self.data.POST.get('button_id')
+
+    if not button_id:
+      raise exceptions.BadRequest('Missing button_id')
+
+    if button_id == 'publish':
+      return self.postPublish(parsed, True)
+
+    if button_id == 'unpublish':
+      return self.postPublish(parsed, False)
+
+    raise exceptions.BadRequest("Unknown button_id")
+
+  def postPublish(self, data, publish):
+    """Publish or unpublish tasks based on the value in the publish parameter.
+
+    Args:
+      data: Parsed post data containing the list of of task keys
+      publish: True if the task is to be published, False to unpublish
+    """
+    for properties in data:
+      task_key = properties.get('key')
+      if not task_key:
+        logging.warning("Missing key in '%s'" % properties)
+        continue
+      if not task_key.isdigit():
+        logging.warning("Invalid task id in '%s'" % properties)
+        continue
+
+      def publish_task_txn():
+        task = GCITask.get_by_id(int(task_key))
+
+        if not task:
+          logging.warning("Task with task_id '%s' does not exist" % (
+              task_key,))
+          return
+
+        org_key = GCITask.org.get_value_for_datastore(task)
+        if not self.data.orgAdminFor(org_key):
+          logging.warning('Not an org admin')
+          return
+
+        task.status = 'Open' if publish else 'Unpublished'
+        task.put()
+
+      db.run_in_transaction(publish_task_txn)
+    return True
+
   def context(self):
     """Returns the context of this component.
     """