Merge branch 'master' into 4062-infinite-scroll-repeat-issue
authorradhika <radhika@curoverse.com>
Fri, 17 Oct 2014 03:28:29 +0000 (23:28 -0400)
committerradhika <radhika@curoverse.com>
Fri, 17 Oct 2014 03:28:29 +0000 (23:28 -0400)
Conflicts:
services/api/test/fixtures/collections.yml
services/api/test/fixtures/jobs.yml

31 files changed:
apps/workbench/app/helpers/application_helper.rb
apps/workbench/app/models/api_client_authorization.rb
apps/workbench/app/models/arvados_base.rb
apps/workbench/app/models/authorized_key.rb
apps/workbench/app/models/collection.rb
apps/workbench/app/models/job.rb
apps/workbench/app/models/pipeline_instance.rb
apps/workbench/app/models/user.rb
apps/workbench/app/models/virtual_machine.rb
apps/workbench/test/functional/projects_controller_test.rb
apps/workbench/test/test_helper.rb
apps/workbench/test/unit/collection_test.rb
apps/workbench/test/unit/group_test.rb
apps/workbench/test/unit/job_test.rb
apps/workbench/test/unit/pipeline_instance_test.rb
doc/api/schema/Group.html.textile.liquid
doc/api/schema/User.html.textile.liquid
sdk/python/arvados/commands/ws.py [new file with mode: 0644]
sdk/python/arvados/events.py
sdk/python/bin/arv-ws
sdk/python/tests/test_websockets.py
services/api/app/models/arvados_model.rb
services/api/app/models/user.rb
services/api/lib/eventbus.rb
services/api/test/fixtures/api_client_authorizations.yml
services/api/test/fixtures/collections.yml
services/api/test/fixtures/jobs.yml
services/api/test/fixtures/links.yml
services/api/test/fixtures/pipeline_instances.yml
services/api/test/fixtures/users.yml
services/api/test/functional/arvados/v1/users_controller_test.rb

index d343249eaf7bef019fdd5efda4d28ec9521451d2..66b7ed662abd86416f13e4b45a10faa5a786d8e0 100644 (file)
@@ -150,9 +150,7 @@ module ApplicationHelper
 
   def render_editable_attribute(object, attr, attrvalue=nil, htmloptions={})
     attrvalue = object.send(attr) if attrvalue.nil?
-    if !object.attribute_editable?(attr, :ever) or
-        (!object.editable? and
-         !object.owner_uuid.in?(my_projects.collect(&:uuid)))
+    if not object.attribute_editable?(attr)
       if attrvalue && attrvalue.length > 0
         return render_attribute_as_textile( object, attr, attrvalue, false )
       else
@@ -241,10 +239,7 @@ module ApplicationHelper
       preconfigured_search_str = value_info[:search_for]
     end
 
-    if !object or
-        !object.attribute_editable?(attr, :ever) or
-        (!object.editable? and
-         !object.owner_uuid.in?(my_projects.collect(&:uuid)))
+    if not object.andand.attribute_editable?(attr)
       return link_to_if_arvados_object attrvalue
     end
 
index ac3a9bf8ed53998adcb1ebfdcc6dc038fdc84136..6d1558cc6eb28438003b2599571679370eb6b845 100644 (file)
@@ -1,6 +1,6 @@
 class ApiClientAuthorization < ArvadosBase
-  def attribute_editable? attr, *args
-    ['expires_at', 'default_owner_uuid'].index attr
+  def editable_attributes
+    %w(expires_at default_owner_uuid)
   end
   def self.creatable?
     false
index e0e93b9e2d0828cef0149f95fe51c817260ef3c7..f5be0e1edcba20ddfb1f80f4ece1912eac0d5dfd 100644 (file)
@@ -329,11 +329,20 @@ class ArvadosBase < ActiveRecord::Base
      (current_user.is_admin or
       current_user.uuid == self.owner_uuid or
       new_record? or
-      (writable_by.include? current_user.uuid rescue false))) or false
+      (respond_to?(:writable_by) ?
+       writable_by.include?(current_user.uuid) :
+       (ArvadosBase.find(owner_uuid).writable_by.include? current_user.uuid rescue false)))) or false
+  end
+
+  # Array of strings that are the names of attributes that can be edited
+  # with X-Editable.
+  def editable_attributes
+    self.class.columns.map(&:name) -
+      %w(created_at modified_at modified_by_user_uuid modified_by_client_uuid updated_at)
   end
 
   def attribute_editable?(attr, ever=nil)
-    if %w(created_at modified_at modified_by_user_uuid modified_by_client_uuid updated_at).include? attr.to_s
+    if not editable_attributes.include?(attr.to_s)
       false
     elsif not (current_user.andand.is_active)
       false
index 724c996e4a4ef173f432514349921ab249eddf2a..2d804e1a5345743957b71aa7389eb15ab223312c 100644 (file)
@@ -1,7 +1,7 @@
 class AuthorizedKey < ArvadosBase
-  def attribute_editable? attr, *args
-    if attr.to_s == 'authorized_user_uuid'
-      current_user and current_user.is_admin
+  def attribute_editable?(attr, ever=nil)
+    if (attr.to_s == 'authorized_user_uuid') and (not ever)
+      current_user.andand.is_admin
     else
       super
     end
index 87a083e24be4ee62d3e52cc5336bb6516ef2d5b2..b5347dce00f24c64a388baac1dd9011935ab99c9 100644 (file)
@@ -66,12 +66,8 @@ class Collection < ArvadosBase
     dir_to_tree.call('.')
   end
 
-  def attribute_editable? attr, *args
-    if %w(name description manifest_text).include? attr.to_s
-      true
-    else
-      super
-    end
+  def editable_attributes
+    %w(name description manifest_text)
   end
 
   def self.creatable?
index 977eef91bf278ac5e9a528e935262dce478e4fa5..c59bb89fe851306c80278b4b96ce192c9e064ea7 100644 (file)
@@ -7,12 +7,8 @@ class Job < ArvadosBase
     "#{script} job"
   end
 
-  def attribute_editable? attr, *args
-    if attr.to_sym == :description
-      super && attr.to_sym == :description
-    else
-      false
-    end
+  def editable_attributes
+    %w(description)
   end
 
   def self.creatable?
@@ -42,7 +38,7 @@ class Job < ArvadosBase
     arvados_api_client.api("jobs/", "queue_size", {"_method"=> "GET"})[:queue_size] rescue 0
   end
 
-  def self.queue 
+  def self.queue
     arvados_api_client.unpack_api_response arvados_api_client.api("jobs/", "queue", {"_method"=> "GET"})
   end
 
index 936905713e44891f22261bf6e1c0ba19e598ae75..83328b9e52ce31dd126812ba874de766282fb93c 100644 (file)
@@ -47,10 +47,12 @@ class PipelineInstance < ArvadosBase
     end
   end
 
-  def attribute_editable? attr, *args
-    super && (attr.to_sym == :name || attr.to_sym == :description ||
-              (attr.to_sym == :components and
-               (self.state == 'New' || self.state == 'Ready')))
+  def editable_attributes
+    %w(name description components)
+  end
+
+  def attribute_editable?(name, ever=nil)
+    (ever or %w(New Ready).include?(state)) and super
   end
 
   def attributes_for_display
index 967ea2ad7d238aebfc9f8df1d4b6e171949479cf..7aaa4fe93951ca831add8b7bae6778e251b8b871 100644 (file)
@@ -35,8 +35,9 @@ class User < ArvadosBase
     super.reject { |k,v| %w(owner_uuid default_owner_uuid identity_url prefs).index k }
   end
 
-  def attribute_editable? attr, *args
-    (not (self.uuid.andand.match(/000000000000000$/) and self.is_admin)) and super
+  def attribute_editable?(attr, ever=nil)
+    (ever or not (self.uuid.andand.match(/000000000000000$/) and
+                  self.is_admin)) and super
   end
 
   def friendly_link_name lookup=nil
index 083aae31ecb10c0f7b5b97e0e6d7b9c5129cff2e..3b44397df5459efb7074f46bd094826192e061eb 100644 (file)
@@ -6,8 +6,8 @@ class VirtualMachine < ArvadosBase
   def attributes_for_display
     super.append ['current_user_logins', @current_user_logins]
   end
-  def attribute_editable? attr, *args
-    attr != 'current_user_logins' and super
+  def editable_attributes
+    super - %w(current_user_logins)
   end
   def self.attribute_info
     merger = ->(k,a,b) { a.merge(b, &merger) }
index d76430cfdf523f246e4a60d03abf3f85e2b9a85c..8eb0cdcf04ec86654bf928c369b3da6d761fc2e9 100644 (file)
@@ -93,6 +93,23 @@ class ProjectsControllerTest < ActionController::TestCase
     refute user_can_manage(:project_viewer, "asubproject")
   end
 
+  test "subproject_admin can_manage asubproject" do
+    assert user_can_manage(:subproject_admin, "asubproject")
+  end
+
+  test "project admin can remove items from the project" do
+    coll_key = "collection_to_remove_from_subproject"
+    coll_uuid = api_fixture("collections")[coll_key]["uuid"]
+    delete(:remove_item,
+           { id: api_fixture("groups")["asubproject"]["uuid"],
+             item_uuid: coll_uuid,
+             format: "js" },
+           session_for(:subproject_admin))
+    assert_response :success
+    assert_match(/\b#{coll_uuid}\b/, @response.body,
+                 "removed object not named in response")
+  end
+
   test 'projects#show tab infinite scroll partial obeys limit' do
     get_contents_rows(limit: 1, filters: [['uuid','is_a',['arvados#job']]])
     assert_response :success
index eea81d1b775d2cc29ac07c877a2ff009f5858456..d7fff5de704a58a759b7016d4d66cb75ee4d2ba1 100644 (file)
@@ -38,7 +38,9 @@ class ActiveSupport::TestCase
 
   teardown do
     Thread.current[:arvados_api_token] = nil
+    Thread.current[:user] = nil
     Thread.current[:reader_tokens] = nil
+    Rails.cache.clear
     # Restore configuration settings changed during tests
     $application_config.each do |k,v|
       if k.match /^[^.]*$/
@@ -55,7 +57,7 @@ module ApiFixtureLoader
 
   module ClassMethods
     @@api_fixtures = {}
-    def api_fixture(name)
+    def api_fixture(name, *keys)
       # Returns the data structure from the named API server test fixture.
       @@api_fixtures[name] ||= \
       begin
@@ -66,10 +68,16 @@ module ApiFixtureLoader
         file = file[0, trim_index] if trim_index
         YAML.load(file)
       end
+      keys.inject(@@api_fixtures[name]) { |hash, key| hash[key] }
     end
   end
-  def api_fixture name
-    self.class.api_fixture name
+  def api_fixture(name, *keys)
+    self.class.api_fixture(name, *keys)
+  end
+
+  def find_fixture(object_class, name)
+    object_class.find(api_fixture(object_class.to_s.pluralize.underscore,
+                                  name, "uuid"))
   end
 end
 
index 512ad47c34dc3c5a12e7b92b40af102534c4355e..e71f9667ec97e75ac9960bed17636da618bbc02d 100644 (file)
@@ -40,4 +40,35 @@ class CollectionTest < ActiveSupport::TestCase
                  get_files_tree('multilevel_collection_2'),
                  "Collection file tree was malformed")
   end
+
+  test "portable_data_hash never editable" do
+    refute(Collection.new.attribute_editable?("portable_data_hash", :ever))
+  end
+
+  test "admin can edit name" do
+    use_token :admin
+    assert(find_fixture(Collection, "foo_file").attribute_editable?("name"),
+           "admin not allowed to edit collection name")
+  end
+
+  test "project owner can edit name" do
+    use_token :active
+    assert(find_fixture(Collection, "foo_collection_in_aproject")
+             .attribute_editable?("name"),
+           "project owner not allowed to edit collection name")
+  end
+
+  test "project admin can edit name" do
+    use_token :subproject_admin
+    assert(find_fixture(Collection, "baz_file_in_asubproject")
+             .attribute_editable?("name"),
+           "project admin not allowed to edit collection name")
+  end
+
+  test "project viewer cannot edit name" do
+    use_token :project_viewer
+    refute(find_fixture(Collection, "foo_collection_in_aproject")
+             .attribute_editable?("name"),
+           "project viewer allowed to edit collection name")
+  end
 end
index 3f5cebc9551823fb88c92f5cf09e17c9a188f2ee..4a4530ca5694114f465f50ab0dec2250916a90ea 100644 (file)
@@ -25,4 +25,16 @@ class GroupTest < ActiveSupport::TestCase
       assert_nil user.owner_uuid
     end
   end
+
+  test "project editable by its admin" do
+    use_token :subproject_admin
+    project = Group.find(api_fixture("groups")["asubproject"]["uuid"])
+    assert(project.editable?, "project not editable by admin")
+  end
+
+  test "project not editable by reader" do
+    use_token :project_viewer
+    project = Group.find(api_fixture("groups")["aproject"]["uuid"])
+    refute(project.editable?, "project editable by reader")
+  end
 end
index 5079316934eac12b586c564b52a8a8194257d85c..add4c0fd55b096a3dc829238ba8cfaefc7573ba7 100644 (file)
@@ -1,7 +1,31 @@
 require 'test_helper'
 
 class JobTest < ActiveSupport::TestCase
-  # test "the truth" do
-  #   assert true
-  # end
+  test "admin can edit description" do
+    use_token :admin
+    assert(find_fixture(Job, "job_in_subproject")
+             .attribute_editable?("description"),
+           "admin not allowed to edit job description")
+  end
+
+  test "project owner can edit description" do
+    use_token :active
+    assert(find_fixture(Job, "job_in_subproject")
+             .attribute_editable?("description"),
+           "project owner not allowed to edit job description")
+  end
+
+  test "project admin can edit description" do
+    use_token :subproject_admin
+    assert(find_fixture(Job, "job_in_subproject")
+             .attribute_editable?("description"),
+           "project admin not allowed to edit job description")
+  end
+
+  test "project viewer cannot edit description" do
+    use_token :project_viewer
+    refute(find_fixture(Job, "job_in_subproject")
+             .attribute_editable?("description"),
+           "project viewer allowed to edit job description")
+  end
 end
index 9b4c7c3787b26aa11545e402b6bc2c847fd86cc4..95ad8fa7cd11bf4abaabd7d04712945071255d28 100644 (file)
@@ -1,7 +1,31 @@
 require 'test_helper'
 
 class PipelineInstanceTest < ActiveSupport::TestCase
-  # test "the truth" do
-  #   assert true
-  # end
+  test "admin can edit name" do
+    use_token :admin
+    assert(find_fixture(PipelineInstance, "new_pipeline_in_subproject")
+             .attribute_editable?("name"),
+           "admin not allowed to edit pipeline instance name")
+  end
+
+  test "project owner can edit name" do
+    use_token :active
+    assert(find_fixture(PipelineInstance, "new_pipeline_in_subproject")
+             .attribute_editable?("name"),
+           "project owner not allowed to edit pipeline instance name")
+  end
+
+  test "project admin can edit name" do
+    use_token :subproject_admin
+    assert(find_fixture(PipelineInstance, "new_pipeline_in_subproject")
+             .attribute_editable?("name"),
+           "project admin not allowed to edit pipeline instance name")
+  end
+
+  test "project viewer cannot edit name" do
+    use_token :project_viewer
+    refute(find_fixture(PipelineInstance, "new_pipeline_in_subproject")
+             .attribute_editable?("name"),
+           "project viewer allowed to edit pipeline instance name")
+  end
 end
index 8a542650d2ea482ff3198a8ff3c13824c54dd332..2bf67eb6ffc4d0edff8606613f5748b0d1450ae1 100644 (file)
@@ -22,3 +22,4 @@ table(table table-bordered table-condensed).
 |group_class|string|Type of group. This does not affect behavior, but determines how the group is presented in the user interface. For example, @project@ indicates that the group should be displayed by Workbench and arv-mount as a project for organizing and naming objects.|@"project"@
 null|
 |description|text|||
+|writable_by|array|List of UUID strings identifying Users and other Groups that have write permission for this Group.  Only users who are allowed to administer the Group will receive a full list.  Other users will receive a partial list that includes the Group's owner_uuid and (if applicable) their own user UUID.||
index c95a2439a24204a9db7bfbddb80cb84ab99f9c7e..9a1b0566d4b0861e340e3d198de81ccb60e67e38 100644 (file)
@@ -26,3 +26,4 @@ table(table table-bordered table-condensed).
 |prefs|hash|||
 |default_owner_uuid|string|||
 |is_active|boolean|||
+|writable_by|array|List of UUID strings identifying Groups and other Users that can modify this User object.  This will include the user's owner_uuid and, for administrators and users requesting their own User object, the requesting user's UUID.||
diff --git a/sdk/python/arvados/commands/ws.py b/sdk/python/arvados/commands/ws.py
new file mode 100644 (file)
index 0000000..674daad
--- /dev/null
@@ -0,0 +1,94 @@
+#!/usr/bin/env python
+
+import sys
+import logging
+import argparse
+import arvados
+import json
+from arvados.events import subscribe
+import signal
+
+def main(arguments=None):
+    logger = logging.getLogger('arvados.arv-ws')
+
+    parser = argparse.ArgumentParser()
+    parser.add_argument('-u', '--uuid', type=str, default="", help="Filter events on object_uuid")
+    parser.add_argument('-f', '--filters', type=str, default="", help="Arvados query filter to apply to log events (JSON encoded)")
+
+    group = parser.add_mutually_exclusive_group()
+    group.add_argument('--poll-interval', default=15, type=int, help="If websockets is not available, specify the polling interval, default is every 15 seconds")
+    group.add_argument('--no-poll', action='store_false', dest='poll_interval', help="Do not poll if websockets are not available, just fail")
+
+    group = parser.add_mutually_exclusive_group()
+    group.add_argument('-p', '--pipeline', type=str, default="", help="Supply pipeline uuid, print log output from pipeline and its jobs")
+    group.add_argument('-j', '--job', type=str, default="", help="Supply job uuid, print log output from jobs")
+
+    args = parser.parse_args(arguments)
+
+    global filters
+    global known_component_jobs
+    global ws
+
+    filters = []
+    known_component_jobs = set()
+    ws = None
+
+    def update_subscribed_components(components):
+        global known_component_jobs
+        global filters
+        pipeline_jobs = set()
+        for c in components:
+            if "job" in components[c]:
+                pipeline_jobs.add(components[c]["job"]["uuid"])
+        if known_component_jobs != pipeline_jobs:
+            ws.unsubscribe(filters)
+            filters = [['object_uuid', 'in', [args.pipeline] + list(pipeline_jobs)]]
+            ws.subscribe([['object_uuid', 'in', [args.pipeline] + list(pipeline_jobs)]])
+            known_component_jobs = pipeline_jobs
+
+    api = arvados.api('v1', cache=False)
+
+    if args.uuid:
+        filters += [ ['object_uuid', '=', args.uuid] ]
+
+    if args.filters:
+        filters += json.loads(args.filters)
+
+    if args.job:
+        filters += [ ['object_uuid', '=', args.job] ]
+
+    if args.pipeline:
+        filters += [ ['object_uuid', '=', args.pipeline] ]
+
+    def on_message(ev):
+        global filters
+        global ws
+
+        logger.debug(ev)
+        if 'event_type' in ev and (args.pipeline or args.job):
+            if ev['event_type'] in ('stderr', 'stdout'):
+                sys.stdout.write(ev["properties"]["text"])
+            elif ev["event_type"] in ("create", "update"):
+                if ev["object_kind"] == "arvados#pipelineInstance":
+                    update_subscribed_components(ev["properties"]["new_attributes"]["components"])
+        elif 'status' in ev and ev['status'] == 200:
+            pass
+        else:
+            print json.dumps(ev)
+
+    try:
+        ws = subscribe(arvados.api('v1', cache=False), filters, on_message, poll_fallback=args.poll_interval)
+        if ws:
+            if args.pipeline:
+                c = api.pipeline_instances().get(uuid=args.pipeline).execute()
+                update_subscribed_components(c["components"])
+
+            while True:
+                signal.pause()
+    except KeyboardInterrupt:
+        pass
+    except Exception as e:
+        logger.error(e)
+    finally:
+        if ws:
+            ws.close()
index b7d610d66e729a9b9cb3a27b29a9abc51395493b..e6038fcd7d7c7b38845dec0a8653b19f259eb3f5 100644 (file)
@@ -1,5 +1,5 @@
 from ws4py.client.threadedclient import WebSocketClient
-import thread
+import threading
 import json
 import os
 import time
@@ -7,6 +7,7 @@ import ssl
 import re
 import config
 import logging
+import arvados
 
 _logger = logging.getLogger('arvados.events')
 
@@ -18,13 +19,12 @@ class EventClient(WebSocketClient):
             ssl_options={'cert_reqs': ssl.CERT_NONE}
         else:
             ssl_options={'cert_reqs': ssl.CERT_REQUIRED}
-
-        super(EventClient, self).__init__(url, ssl_options)
+        super(EventClient, self).__init__(url, ssl_options=ssl_options)
         self.filters = filters
         self.on_event = on_event
 
     def opened(self):
-        self.send(json.dumps({"method": "subscribe", "filters": self.filters}))
+        self.subscribe(self.filters)
 
     def received_message(self, m):
         self.on_event(json.loads(str(m)))
@@ -36,14 +36,83 @@ class EventClient(WebSocketClient):
         except:
             pass
 
-def subscribe(api, filters, on_event):
+    def subscribe(self, filters, last_log_id=None):
+        m = {"method": "subscribe", "filters": filters}
+        if last_log_id is not None:
+            m["last_log_id"] = last_log_id
+        self.send(json.dumps(m))
+
+    def unsubscribe(self, filters):
+        self.send(json.dumps({"method": "unsubscribe", "filters": filters}))
+
+class PollClient(threading.Thread):
+    def __init__(self, api, filters, on_event, poll_time):
+        super(PollClient, self).__init__()
+        self.api = api
+        if filters:
+            self.filters = [filters]
+        else:
+            self.filters = [[]]
+        self.on_event = on_event
+        self.poll_time = poll_time
+        self.stop = threading.Event()
+
+    def run(self):
+        self.id = 0
+        for f in self.filters:
+            items = self.api.logs().list(limit=1, order="id desc", filters=f).execute()['items']
+            if items:
+                if items[0]['id'] > self.id:
+                    self.id = items[0]['id']
+
+        self.on_event({'status': 200})
+
+        while not self.stop.isSet():
+            max_id = self.id
+            for f in self.filters:
+                items = self.api.logs().list(order="id asc", filters=f+[["id", ">", str(self.id)]]).execute()['items']
+                for i in items:
+                    if i['id'] > max_id:
+                        max_id = i['id']
+                    self.on_event(i)
+            self.id = max_id
+            self.stop.wait(self.poll_time)
+
+    def close(self):
+        self.stop.set()
+        self.join()
+
+    def subscribe(self, filters):
+        self.on_event({'status': 200})
+        self.filters.append(filters)
+
+    def unsubscribe(self, filters):
+        del self.filters[self.filters.index(filters)]
+
+
+def subscribe(api, filters, on_event, poll_fallback=15):
+    '''
+    api: Must be a newly created from arvados.api(cache=False), not shared with the caller, as it may be used by a background thread.
+    filters: Initial subscription filters.
+    on_event: The callback when a message is received
+    poll_fallback: If websockets are not available, fall back to polling every N seconds.  If poll_fallback=False, this will return None if websockets are not available.
+    '''
     ws = None
-    try:
-        url = "{}?api_token={}".format(api._rootDesc['websocketUrl'], config.get('ARVADOS_API_TOKEN'))
-        ws = EventClient(url, filters, on_event)
-        ws.connect()
-        return ws
-    except Exception:
-        if (ws):
-          ws.close_connection()
-        raise
+    if 'websocketUrl' in api._rootDesc:
+        try:
+            url = "{}?api_token={}".format(api._rootDesc['websocketUrl'], api.api_token)
+            ws = EventClient(url, filters, on_event)
+            ws.connect()
+            return ws
+        except Exception as e:
+            _logger.warn("Got exception %s trying to connect to websockets at %s" % (e, api._rootDesc['websocketUrl']))
+            if ws:
+                ws.close_connection()
+    if poll_fallback:
+        _logger.warn("Websockets not available, falling back to log table polling")
+        p = PollClient(api, filters, on_event, poll_fallback)
+        p.start()
+        return p
+    else:
+        _logger.error("Websockets not available")
+        return None
index ce7f066ec7682ff8b4fe3cdeaa59e0656db68716..4e663cef1bc2bd705ed7651c3185aa743637b4b7 100755 (executable)
@@ -1,30 +1,4 @@
 #!/usr/bin/env python
 
-import sys
-import logging
-import argparse
-import arvados
-from arvados.events import subscribe
-
-logger = logging.getLogger('arvados.arv-ws')
-
-parser = argparse.ArgumentParser()
-parser.add_argument('-u', '--uuid', type=str, default="")
-args = parser.parse_args()
-
-filters = []
-if len(args.uuid)>0: filters = [ ['object_uuid', '=', args.uuid] ]
-
-api = arvados.api('v1', cache=False)
-
-def on_message(ev):
-  print "\n", ev
-
-ws = None
-try:
-  ws = subscribe(api, filters, lambda ev: on_message(ev))
-  ws.run_forever()
-except Exception:
-  logger.exception('')
-  if (ws):
-    ws.close_connection()
+from arvados.commands.ws import main
+main()
index 1dae978c843b052a19469c1e8dfc0ee876a0ede9..032ac51f0d445a5b03e751cf569f5c835307c367 100644 (file)
@@ -2,27 +2,51 @@ import run_test_server
 import unittest
 import arvados
 import arvados.events
-import time
-
-class WebsocketTest(run_test_server.TestCaseWithServers):
-    MAIN_SERVER = {'websockets': True}
+import threading
 
+class EventTestBase(object):
     def on_event(self, ev):
         if self.state == 1:
             self.assertEqual(200, ev['status'])
             self.state = 2
+            self.subscribed.set()
         elif self.state == 2:
             self.assertEqual(self.h[u'uuid'], ev[u'object_uuid'])
             self.state = 3
+            self.done.set()
         elif self.state == 3:
             self.fail()
 
     def runTest(self):
+        self.ws = None
         self.state = 1
+        self.subscribed = threading.Event()
+        self.done = threading.Event()
 
         run_test_server.authorize_with("admin")
         api = arvados.api('v1', cache=False)
-        arvados.events.subscribe(api, [['object_uuid', 'is_a', 'arvados#human']], lambda ev: self.on_event(ev))
-        time.sleep(1)
+        self.ws = arvados.events.subscribe(arvados.api('v1', cache=False), [['object_uuid', 'is_a', 'arvados#human']], self.on_event, poll_fallback=2)
+        self.assertIsInstance(self.ws, self.WS_TYPE)
+        self.subscribed.wait(10)
         self.h = api.humans().create(body={}).execute()
-        time.sleep(1)
+        self.done.wait(10)
+        self.assertEqual(3, self.state)
+
+class WebsocketTest(run_test_server.TestCaseWithServers, EventTestBase):
+    MAIN_SERVER = {'websockets': True}
+    WS_TYPE = arvados.events.EventClient
+
+    def tearDown(self):
+        if self.ws:
+            self.ws.close()
+        super(WebsocketTest, self).tearDown()
+
+
+class PollClientTest(run_test_server.TestCaseWithServers, EventTestBase):
+    MAIN_SERVER = {}
+    WS_TYPE = arvados.events.PollClient
+
+    def tearDown(self):
+        if self.ws:
+            self.ws.close()
+        super(PollClientTest, self).tearDown()
index 823fd55435b7d4555395a64ab23dd519d5ed1917..13ccd7033560318c2db2687928a08a4e5de0d44e 100644 (file)
@@ -110,7 +110,8 @@ class ArvadosModel < ActiveRecord::Base
     unless (owner_uuid == current_user.uuid or
             current_user.is_admin or
             (current_user.groups_i_can(:manage) & [uuid, owner_uuid]).any?)
-      if current_user.groups_i_can(:write).index(uuid)
+      if ((current_user.groups_i_can(:write) + [current_user.uuid]) &
+          [uuid, owner_uuid]).any?
         return [owner_uuid, current_user.uuid]
       else
         return [owner_uuid]
index 6e7facd5d550ee45bd948a255e28e5b18ddec6cb..ecd50ccdc4f3b0c601631f96bc89eaa16eebf0a7 100644 (file)
@@ -29,6 +29,7 @@ class User < ArvadosModel
     t.add :is_admin
     t.add :is_invited
     t.add :prefs
+    t.add :writable_by
   end
 
   ALL_PERMISSIONS = {read: true, write: true, manage: true}
index 03afc3160159727086fc56f3766d51832790e38e..bccbeea4bb62ed951cdcc3f6a6ad2d2ac9098d7d 100644 (file)
@@ -129,7 +129,7 @@ class EventBus
           # Add a filter.  This gets the :filters field which is the same
           # format as used for regular index queries.
           ws.filters << Filter.new(p)
-          ws.send ({status: 200, message: 'subscribe ok'}.to_json)
+          ws.send ({status: 200, message: 'subscribe ok', filter: p}.to_json)
 
           # Send any pending events
           push_events ws
index f334281a62ee35d3abf7c0510e301c20a548b709..3b5df3795c097bdd33b026a2599eb32f10cacb34 100644 (file)
@@ -49,6 +49,12 @@ project_viewer:
   api_token: projectviewertoken1234567890abcdefghijklmnopqrstuv
   expires_at: 2038-01-01 00:00:00
 
+subproject_admin:
+  api_client: untrusted
+  user: subproject_admin
+  api_token: subprojectadmintoken1234567890abcdefghijklmnopqrst
+  expires_at: 2038-01-01 00:00:00
+
 admin_vm:
   api_client: untrusted
   user: admin
index abc1eea9cbb83ca1db507fcb58f7f0de30573f94..c481424d6d29160778ee7843f2da9bb4a65994a8 100644 (file)
@@ -258,6 +258,15 @@ collection_owned_by_foo:
   created_at: 2014-02-03T17:22:54Z
   name: collection_owned_by_foo
 
+collection_to_remove_from_subproject:
+  # The Workbench tests remove this from subproject.
+  uuid: zzzzz-4zz18-subprojgonecoll
+  portable_data_hash: 2386ca6e3fffd4be5e197a72c6c80fb2+51
+  manifest_text: ". 8258b505536a9ab47baa2f4281cb932a+9 0:9:missingno\n"
+  owner_uuid: zzzzz-j7d0g-axqo7eu9pwvna1x
+  created_at: 2014-10-15T10:45:00
+  name: Collection to remove from subproject
+
 # Test Helper trims the rest of the file
 
 # Do not add your fixtures below this line as the rest of this file will be trimmed by test_helper
index abcaad1ac904ecdcae1af6d1fa8e81b46904a510..292c27e2e37057848050f2d81da7e79d89eb0d53 100644 (file)
@@ -285,6 +285,16 @@ cancelled:
   runtime_constraints: {}
   state: Cancelled
 
+job_in_subproject:
+  uuid: zzzzz-8i9sb-subprojectjob01
+  created_at: 2014-10-15 12:00:00
+  owner_uuid: zzzzz-j7d0g-axqo7eu9pwvna1x
+  log: ~
+  repository: foo
+  script: hash
+  script_version: 4fe459abe02d9b365932b8f5dc419439ab4e2577
+  state: Complete
+
 # Test Helper trims the rest of the file
 
 # Do not add your fixtures below this line as the rest of this file will be trimmed by test_helper
index bca925fca188ab7f873f13f708cc646bb96edd06..899e9f0b3b615bb0df5a8c73f56c67db1f650b54 100644 (file)
@@ -377,6 +377,20 @@ project_viewer_can_read_project:
   head_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
   properties: {}
 
+subproject_admin_can_manage_subproject:
+  uuid: zzzzz-o0j2j-subprojadminlnk
+  owner_uuid: zzzzz-tpzed-000000000000000
+  created_at: 2014-10-15 10:00:00 -0000
+  modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+  modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+  modified_at: 2014-10-15 10:00:00 -0000
+  updated_at: 2014-10-15 10:00:00 -0000
+  tail_uuid: zzzzz-tpzed-subprojectadmin
+  link_class: permission
+  name: can_manage
+  head_uuid: zzzzz-j7d0g-axqo7eu9pwvna1x
+  properties: {}
+
 foo_collection_tag:
   uuid: zzzzz-o0j2j-eedahfaho8aphiv
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
index d49db202e52263a06fe3b3c1a6f2190092b67b59..fd616cde932423bf0bd511638a10a5618d0e2154 100644 (file)
@@ -4,6 +4,12 @@ new_pipeline:
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: <%= 1.minute.ago.to_s(:db) %>
 
+new_pipeline_in_subproject:
+  state: New
+  uuid: zzzzz-d1hrv-subprojpipeline
+  owner_uuid: zzzzz-j7d0g-axqo7eu9pwvna1x
+  created_at: <%= 1.minute.ago.to_s(:db) %>
+
 has_component_with_no_script_parameters:
   state: Ready
   uuid: zzzzz-d1hrv-1xfj6xkicf2muk2
index 92fce2c7d5b746164584ec3ef6283e517fbb01f7..17ae82e901246bab0471efa26cc6fa454ff27cdd 100644 (file)
@@ -85,6 +85,20 @@ future_project_user:
       organization: example.com
       role: Computational biologist
 
+subproject_admin:
+  owner_uuid: zzzzz-tpzed-000000000000000
+  uuid: zzzzz-tpzed-subprojectadmin
+  email: subproject-admin@arvados.local
+  first_name: Subproject
+  last_name: Admin
+  identity_url: https://subproject-admin.openid.local
+  is_active: true
+  is_admin: false
+  prefs:
+    profile:
+      organization: example.com
+      role: Computational biologist
+
 spectator:
   owner_uuid: zzzzz-tpzed-000000000000000
   uuid: zzzzz-tpzed-l1s2piq4t4mps8r
index 9c4d18b24152012f8161ec8ab0c0d1521c611413..2d26370b749f5b07dc866855563c62cd31f9c03a 100644 (file)
@@ -779,6 +779,16 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     assert_equal false, found_email, 'Expected no email after updating profile'
   end
 
+  test "user API response includes writable_by" do
+    authorize_with :active
+    get :current
+    assert_response :success
+    assert_includes(json_response["writable_by"], users(:active).uuid,
+                    "user's writable_by should include self")
+    assert_includes(json_response["writable_by"], users(:active).owner_uuid,
+                    "user's writable_by should include its owner_uuid")
+  end
+
 
   NON_ADMIN_USER_DATA = ["uuid", "kind", "is_active", "email", "first_name",
                          "last_name"].sort