filters: [['group_class', '=', 'project']],
description: 'project',
},
+ {
+ wb_path: 'projects',
+ api_path: 'arvados/v1/groups',
+ filters: [['group_class', '=', 'filter']],
+ description: 'project',
+ },
{
wb_path: 'collections',
api_path: 'arvados/v1/collections',
@object.link_class == 'name' and
ArvadosBase::resource_class_for_uuid(@object.head_uuid) == Collection
redirect_to collection_path(id: @object.uuid)
- elsif @object.is_a?(Group) and @object.group_class == 'project'
+ elsif @object.is_a?(Group) and (@object.group_class == 'project' or @object.group_class == 'filter')
redirect_to project_path(id: @object.uuid)
elsif @object
redirect_to @object
# exception here than in a template.)
unless current_user.nil?
begin
- my_starred_projects current_user
+ my_starred_projects current_user, 'project'
build_my_wanted_projects_tree current_user
rescue ArvadosApiClient::ApiError
# Fall back to the default-setting code later.
if objects.respond_to?(:result_offset) and
objects.respond_to?(:result_limit)
next_offset = objects.result_offset + objects.result_limit
- if objects.respond_to?(:items_available) and (next_offset < objects.items_available)
+ if objects.respond_to?(:items_available) and (objects.items_available != nil) and (next_offset < objects.items_available)
next_offset
elsif @objects.results.size > 0 and (params[:count] == 'none' or
(params[:controller] == 'search' and params[:action] == 'choose'))
helper_method :all_projects
def all_projects
@all_projects ||= Group.
- filter([['group_class','=','project']]).order('name')
+ filter([['group_class','IN',['project','filter']]]).order('name')
end
helper_method :my_projects
end
helper_method :my_starred_projects
- def my_starred_projects user
+ def my_starred_projects user, group_class
return if defined?(@starred_projects) && @starred_projects
links = Link.filter([['owner_uuid', 'in', ["#{Rails.configuration.ClusterID}-j7d0g-publicfavorites", user.uuid]],
['link_class', '=', 'star'],
['head_uuid', 'is_a', 'arvados#group']]).with_count("none").select(%w(head_uuid))
uuids = links.collect { |x| x.head_uuid }
- starred_projects = Group.filter([['uuid', 'in', uuids]]).order('name').with_count("none")
+ if group_class == ""
+ starred_projects = Group.filter([['uuid', 'in', uuids]]).order('name').with_count("none")
+ else
+ starred_projects = Group.filter([['uuid', 'in', uuids],['group_class', '=', group_class]]).order('name').with_count("none")
+ end
@starred_projects = starred_projects.results
end
@too_many_projects = false
@reached_level_limit = false
while from_top.size <= page_size*2
- current_level = Group.filter([['group_class','=','project'],
+ current_level = Group.filter([['group_class','IN',['project','filter']],
['owner_uuid', 'in', uuids]])
.order('name').limit(page_size*2)
break if current_level.results.size == 0
class GroupsController < ApplicationController
def index
- @groups = Group.filter [['group_class', '!=', 'project']]
+ @groups = Group.filter [['group_class', '!=', 'project'], ['group_class', '!=', 'filter']]
@group_uuids = @groups.collect &:uuid
@links_from = Link.where(link_class: 'permission', tail_uuid: @group_uuids).with_count("none")
@links_to = Link.where(link_class: 'permission', head_uuid: @group_uuids).with_count("none")
end
def show
- if @object.group_class == 'project'
+ if @object.group_class == 'project' or @object.group_class == 'filter'
redirect_to(project_path(@object))
else
super
raw(link_name)
else
controller_class = resource_class.to_s.tableize
- if controller_class.eql?('groups') and object.andand.group_class.eql?('project')
+ if controller_class.eql?('groups') and (object.andand.group_class.eql?('project') or object.andand.group_class.eql?('filter'))
controller_class = 'projects'
end
(link_to raw(link_name), { controller: controller_class, action: 'show', id: ((opts[:name_link].andand.uuid) || link_uuid) }, style_opts) + raw(tags)
ret
end
+ def editable?
+ if group_class == 'filter'
+ return false
+ end
+ super
+ end
+
def contents params={}
res = arvados_api_client.api self.class, "/#{self.uuid}/contents", {
_method: 'GET'
end
def class_for_display
- group_class == 'project' ? 'Project' : super
+ (group_class == 'project' or group_class == 'filter') ? 'Project' : super
end
def textile_attributes
SPDX-License-Identifier: AGPL-3.0 %>
-<% starred_projects = my_starred_projects current_user%>
+<% starred_projects = my_starred_projects current_user, '' %>
<% if starred_projects.andand.any? %>
<li role="presentation" class="dropdown-header">
My favorite projects
<div class="modal-body">
<div class="selectable-container" style="height: 15em; overflow-y: scroll">
- <% starred_projects = my_starred_projects current_user%>
+ <% starred_projects = my_starred_projects current_user, 'project' %>
<% if starred_projects.andand.any? %>
<% writable_projects = starred_projects.select(&:editable?) %>
<% writable_projects.each do |projectnode| %>
<%= render_editable_attribute @object, 'name', nil, { 'data-emptytext' => "New project" } %>
<% end %>
</h2>
+ <% if @object.class == Group and @object.group_class == 'filter' %>
+ This is a filter group.
+ <% end %>
<% end %>
<%
use_token user
ctrl = ProjectsController.new
current_user = User.find(api_fixture('users')[user]['uuid'])
- my_starred_project = ctrl.send :my_starred_projects, current_user
+ my_starred_project = ctrl.send :my_starred_projects, current_user, ''
assert_equal(size, my_starred_project.andand.size)
ctrl2 = ProjectsController.new
current_user = User.find(api_fixture('users')[user]['uuid'])
- my_starred_project = ctrl2.send :my_starred_projects, current_user
+ my_starred_project = ctrl2.send :my_starred_projects, current_user, ''
assert_equal(size, my_starred_project.andand.size)
end
end
use_token :project_viewer
current_user = User.find(api_fixture('users')['project_viewer']['uuid'])
ctrl = ProjectsController.new
- my_starred_project = ctrl.send :my_starred_projects, current_user
+ my_starred_project = ctrl.send :my_starred_projects, current_user, ''
assert_equal(0, my_starred_project.andand.size)
# share it again
# verify that the project is again included in starred projects
use_token :project_viewer
ctrl = ProjectsController.new
- my_starred_project = ctrl.send :my_starred_projects, current_user
+ my_starred_project = ctrl.send :my_starred_projects, current_user, ''
assert_equal(1, my_starred_project.andand.size)
end
end
- api/keep-webdav.html.textile.liquid
- api/keep-s3.html.textile.liquid
- api/keep-web-urls.html.textile.liquid
+ - api/projects.html.textile.liquid
- api/methods/collections.html.textile.liquid
- api/methods/repositories.html.textile.liquid
- Container engine:
|@is_a@|string|Arvados object type|@["head_uuid","is_a","arvados#collection"]@|
|@exists@|string|Test if a subproperty is present.|@["properties","exists","my_subproperty"]@|
-Note:
-
h4(#substringsearchfilter). Filtering using substring search
Resources can also be filtered by searching for a substring in attributes of type @string@, @array of strings@, @text@, and @hash@, which are indexed in the database specifically for search. To use substring search, the filter must:
table(table table-bordered table-condensed).
|_. Attribute|_. Type|_. Description|_. Example|
|name|string|||
-|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|
+|group_class|string|Type of group. @project@ and @filter@ indicate that the group should be displayed by Workbench and arv-mount as a project for organizing and naming objects. @role@ is used as part of the "permission system":{{site.baseurl}}/api/permission-model.html. |@"filter"@
+@"project"@
+@"role"@|
|description|text|||
|properties|hash|User-defined metadata, may be used in queries using "subproperty filters":{{site.baseurl}}/api/methods.html#subpropertyfilters ||
|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.||
|delete_at|datetime|If @delete_at@ is non-null and in the past, the group and all objects directly or indirectly owned by the group may be permanently deleted.||
|is_trashed|datetime|True if @trash_at@ is in the past, false if not.||
+@filter@ groups are virtual groups; they can not own other objects. Filter groups have a special @properties@ field named @filters@, which must be an array of filter conditions. See "list method filters":{{site.baseurl}}/api/methods.html#filters for details on the syntax of valid filters, but keep in mind that the attributes must include the object type (@collections@, @container_requests@, @groups@, @workflows@), separated with a dot from the field to be filtered on.
+
+Filters are applied with an implied *and* between them, but each filter only applies to the object type specified. The results are subject to the usual access controls - they are a subset of all objects the user can see. Here is an example:
+
+<pre>
+ "properties":{
+ "filters":[
+ [
+ "groups.name",
+ "like",
+ "Public%"
+ ]
+ ]
+ },
+</pre>
+
+This @filter@ group will return all groups (projects) that have a name starting with the word @Public@ and are visible to the user issuing the query. Because groups can contain many types of object, it will also return all objects of other types that the user can see.
+
+The 'is_a' filter operator is of particular interest to limit the @filter@ group 'content' to the desired object(s). When the 'is_a' operator is used, the attribute must be 'uuid'. The operand may be a string or an array which means objects of either type will match the filter. This example will return all groups (projects) that have a name starting with the word @Public@, as well as all collections that are in the project with uuid @zzzzz-j7d0g-0123456789abcde@.
+
+<pre>
+ "properties":{
+ "filters":[
+ [
+ "groups.name",
+ "like",
+ "Public%"
+ ],
+ [
+ "collections.owner_uuid",
+ "=",
+ "zzzzz-j7d0g-0123456789abcde"
+ ],
+ [
+ "uuid",
+ "is_a",
+ [
+ "arvados#group",
+ "arvados#collection"
+ ]
+ ]
+ ]
+ },
+ </pre>
+
h2. Methods
See "Common resource methods":{{site.baseurl}}/api/methods.html for more information about @create@, @delete@, @get@, @list@, and @update@.
h2. Ownership
-All Arvados objects have an @owner_uuid@ field. Valid uuid types for @owner_uuid@ are "User" and "Group". For Group, the @group_class@ must be a "project".
+All Arvados objects have an @owner_uuid@ field. Valid uuid types for @owner_uuid@ are "User" and "Group". In the case of a Group, the @group_class@ must be "project".
The User or Group specified by @owner_uuid@ has *can_manage* permission on the object. This permission is one way: an object that is owned does not get any special permissions on the User or Group that owns it.
A "project" is a subtype of Group that is displayed as a "Project" in Workbench, and as a directory by @arv-mount@.
* A project can own things (appear in @owner_uuid@)
* A project can be owned by a user or another project.
-* The name of a project is unique only among projects with the same owner_uuid.
+* The name of a project is unique only among projects and filters with the same owner_uuid.
* Projects can be targets (@head_uuid@) of permission links, but not origins (@tail_uuid@). Putting a project in a @tail_uuid@ field is an error.
+A "filter" is a subtype of Group that is displayed as a "Project" in Workbench, and as a directory by @arv-mount@. See "the groups API documentation":/api/methods/groups.html for more information.
+* A filter group cannot own things (cannot appear in @owner_uuid@). Putting a filter group in an @owner_uuid@ field is an error.
+* A filter group can be owned by a user or a project.
+* The name of a filter is unique only among projects and filters with the same owner_uuid.
+* Filters can be targets (@head_uuid@) of permission links, but not origins (@tail_uuid@). Putting a filter in a @tail_uuid@ field is an error.
+
A "role" is a subtype of Group that is treated in Workbench as a group of users who have permissions in common (typically an organizational group).
* A role cannot own things (cannot appear in @owner_uuid@). Putting a role in an @owner_uuid@ field is an error.
* All roles are owned by the system user.
--- /dev/null
+---
+layout: default
+navsection: api
+title: "Projects and filter groups"
+...
+{% comment %}
+Copyright (C) The Arvados Authors. All rights reserved.
+
+SPDX-License-Identifier: CC-BY-SA-3.0
+{% endcomment %}
+
+Arvados @projects@ are used to organize objects. Projects can contain @collections@, @container requests@, @workflows@, etc. Projects can also contain other projects. An object is part of a project if the @owner_uuid@ of the object is set to the uuid of the project.
+
+Projects are implemented as a subtype of the Arvados @group@ object type, with @group_class@ set to the value "project". More information is available in the "groups API reference":/api/methods/groups.html.
+
+Projects can be manipulated via Workbench, the cli tools, the SDKs, and the Arvados APIs.
+
+h2. The home project
+
+Each user has a @home project@, which is implemented differently. This is a virtual project that is comprised of all objects owned by the user, in other words, all objects with the @owner_uuid@ set to the @uuid@ of the user. The home project is accessible via Workbench, which makes it easy view its contents and to move objects from and to the home project. The home project is also accessible via FUSE, WebDAV and the S3 interface.
+
+The same thing can be done via the APIs. To put something in a user's home project via the cli or SDKs, one would set the @owner_uuid@ of the object to the user's @uuid@. This also implies that this user now has full ownership and control over that object.
+
+The contents of the home project can be accessed with the @group contents@ API, e.g. via the cli with this command:
+<pre>arv group contents --uuid zzzzz-tpzed-123456789012345</pre>
+In this command, `zzzzz-tpzed-123456789012345` is a @user@ uuid, which is unusual because we are using it as the argument to a @groups@ API. The @group contents@ API is normally used with a @group@ uuid.
+
+Because the home project is a virtual project, other operations via the @groups@ API are not supported.
+
+h2. Filter groups
+
+Filter groups are another type of virtual project. They are implemented as an Arvados @group@ object with @group_class@ set to the value "filter".
+
+Filter groups define one or more filters which are applied to all objects that the current user can see, and returned as the contents of the @group@. Filter groups are described in more detail in the "groups API reference":{{site.baseurl}}/api/methods/groups.html, and the rules for creating valid filters are the same as for "list method filters":{{site.baseurl}}/api/methods.html#filters.
+
+Filter groups are accessible (read-only) via Workbench and the Arvados FUSE mount, WebDAV and S3 interface. Filter groups must currently be defined via the API, SDK or cli, there is no Workbench support yet.
+
+As an example, create a filter group with the @arv@ cli:
+
+<notextile>
+<pre><code>~$ <span class="userinput"> FILTER_GROUP_UUID=`arv -s group create --group '{
+ "group_class":"filter",
+ "name":"my filter group",
+ "properties":{
+ "filters":
+ [
+ ["collections.name","ilike","%test%"],
+ ["uuid","is_a","arvados#collection"]
+ ]
+ }
+ }'`
+</code>
+</pre>
+</notextile>
+This filter group will contain all collections visible to the current user whose name matches the word @test@ (case insensitive).
+
+To see how this works via the keep FUSE mount, create a few matching (and non-matching) collections:
+
+<notextile>
+<pre><code>~$ <span class="userinput">arv collection create --collection '{"name":"empty test collection 1"}'</span>
+~$ <span class="userinput">arv collection create --collection '{"name":"another empty collection"}'</span>
+~$ <span class="userinput">arv collection create --collection '{"name":"empty Test collection 2"}'</span>
+~$ <span class="userinput">mkdir -p keep</span>
+~$ <span class="userinput">arv-mount keep</span>
+~$ <span class="userinput">ls keep/by_id/$FILTER_GROUP_UUID/ -C1</span>
+'empty test collection 1'
+'empty Test collection 2'</code>
+</pre>
+</notextile>
return conn.chooseBackend(options.UUID).GroupDelete(ctx, options)
}
+func (conn *Conn) GroupTrash(ctx context.Context, options arvados.DeleteOptions) (arvados.Group, error) {
+ return conn.chooseBackend(options.UUID).GroupTrash(ctx, options)
+}
+
func (conn *Conn) GroupUntrash(ctx context.Context, options arvados.UntrashOptions) (arvados.Group, error) {
return conn.chooseBackend(options.UUID).GroupUntrash(ctx, options)
}
import (
"context"
+ "fmt"
+ "strings"
"git.arvados.org/arvados.git/lib/controller/railsproxy"
"git.arvados.org/arvados.git/lib/controller/rpc"
func (conn *Conn) UserAuthenticate(ctx context.Context, opts arvados.UserAuthenticateOptions) (arvados.APIClientAuthorization, error) {
return conn.loginController.UserAuthenticate(ctx, opts)
}
+
+func (conn *Conn) GroupContents(ctx context.Context, options arvados.GroupContentsOptions) (arvados.ObjectList, error) {
+ // The requested UUID can be a user (virtual home project), which we just pass on to
+ // the API server.
+ if strings.Index(options.UUID, "-j7d0g-") != 5 {
+ return conn.railsProxy.GroupContents(ctx, options)
+ }
+
+ var resp arvados.ObjectList
+
+ // Get the group object
+ respGroup, err := conn.GroupGet(ctx, arvados.GetOptions{UUID: options.UUID})
+ if err != nil {
+ return resp, err
+ }
+
+ // If the group has groupClass 'filter', apply the filters before getting the contents.
+ if respGroup.GroupClass == "filter" {
+ if filters, ok := respGroup.Properties["filters"].([]interface{}); ok {
+ for _, f := range filters {
+ // f is supposed to be a []string
+ tmp, ok2 := f.([]interface{})
+ if !ok2 || len(tmp) < 3 {
+ return resp, fmt.Errorf("filter unparsable: %T, %+v, original field: %T, %+v\n", tmp, tmp, f, f)
+ }
+ var filter arvados.Filter
+ if attr, ok2 := tmp[0].(string); ok2 {
+ filter.Attr = attr
+ } else {
+ return resp, fmt.Errorf("filter unparsable: attribute must be string: %T, %+v, filter: %T, %+v\n", tmp[0], tmp[0], f, f)
+ }
+ if operator, ok2 := tmp[1].(string); ok2 {
+ filter.Operator = operator
+ } else {
+ return resp, fmt.Errorf("filter unparsable: operator must be string: %T, %+v, filter: %T, %+v\n", tmp[1], tmp[1], f, f)
+ }
+ filter.Operand = tmp[2]
+ options.Filters = append(options.Filters, filter)
+ }
+ } else {
+ return resp, fmt.Errorf("filter unparsable: not an array\n")
+ }
+ // Use the generic /groups/contents endpoint for filter groups
+ options.UUID = ""
+ }
+
+ return conn.railsProxy.GroupContents(ctx, options)
+}
case *arvados.ListOptions:
rOpts.Select = opts.Select
rOpts.Count = opts.Count
+ case *arvados.GroupContentsOptions:
+ rOpts.Select = opts.Select
+ rOpts.Count = opts.Count
}
return rOpts, nil
}
return rtr.backend.GroupDelete(ctx, *opts.(*arvados.DeleteOptions))
},
},
+ {
+ arvados.EndpointGroupTrash,
+ func() interface{} { return &arvados.DeleteOptions{} },
+ func(ctx context.Context, opts interface{}) (interface{}, error) {
+ return rtr.backend.GroupTrash(ctx, *opts.(*arvados.DeleteOptions))
+ },
+ },
{
arvados.EndpointGroupUntrash,
func() interface{} { return &arvados.UntrashOptions{} },
return resp, err
}
+func (conn *Conn) GroupTrash(ctx context.Context, options arvados.DeleteOptions) (arvados.Group, error) {
+ ep := arvados.EndpointGroupTrash
+ var resp arvados.Group
+ err := conn.requestAndDecode(ctx, &resp, ep, nil, options)
+ return resp, err
+}
+
func (conn *Conn) GroupUntrash(ctx context.Context, options arvados.UntrashOptions) (arvados.Group, error) {
ep := arvados.EndpointGroupUntrash
var resp arvados.Group
EndpointGroupContentsUUIDInPath = APIEndpoint{"GET", "arvados/v1/groups/{uuid}/contents", ""} // Alternative HTTP route; client-side code should always use EndpointGroupContents instead
EndpointGroupShared = APIEndpoint{"GET", "arvados/v1/groups/shared", ""}
EndpointGroupDelete = APIEndpoint{"DELETE", "arvados/v1/groups/{uuid}", ""}
+ EndpointGroupTrash = APIEndpoint{"POST", "arvados/v1/groups/{uuid}/trash", ""}
EndpointGroupUntrash = APIEndpoint{"POST", "arvados/v1/groups/{uuid}/untrash", ""}
EndpointUserActivate = APIEndpoint{"POST", "arvados/v1/users/{uuid}/activate", ""}
EndpointUserCreate = APIEndpoint{"POST", "arvados/v1/users", "user"}
Limit int64 `json:"limit"`
Offset int64 `json:"offset"`
Order []string `json:"order"`
+ Distinct bool `json:"distinct"`
+ Count string `json:"count"`
Include string `json:"include"`
Recursive bool `json:"recursive"`
+ IncludeTrash bool `json:"include_trash"`
IncludeOldVersions bool `json:"include_old_versions"`
ExcludeHomeProject bool `json:"exclude_home_project"`
}
GroupContents(ctx context.Context, options GroupContentsOptions) (ObjectList, error)
GroupShared(ctx context.Context, options ListOptions) (GroupList, error)
GroupDelete(ctx context.Context, options DeleteOptions) (Group, error)
+ GroupTrash(ctx context.Context, options DeleteOptions) (Group, error)
GroupUntrash(ctx context.Context, options UntrashOptions) (Group, error)
SpecimenCreate(ctx context.Context, options CreateOptions) (Specimen, error)
SpecimenUpdate(ctx context.Context, options UpdateOptions) (Specimen, error)
return sc.Client.RequestAndDecode(dst, method, path, body, params)
}
+func (s *SiteFSSuite) TestFilterGroup(c *check.C) {
+ // Make sure that a collection and group that match the filter are present,
+ // and that a group that does not match the filter is not present.
+ s.fs.MountProject("fg", fixtureThisFilterGroupUUID)
+
+ _, err := s.fs.OpenFile("/fg/baz_file", 0, 0)
+ c.Assert(err, check.IsNil)
+
+ _, err = s.fs.OpenFile("/fg/A Subproject", 0, 0)
+ c.Assert(err, check.IsNil)
+
+ _, err = s.fs.OpenFile("/fg/A Project", 0, 0)
+ c.Assert(err, check.Not(check.IsNil))
+
+ // An empty filter means everything that is visible should be returned.
+ s.fs.MountProject("fg2", fixtureAFilterGroupTwoUUID)
+
+ _, err = s.fs.OpenFile("/fg2/baz_file", 0, 0)
+ c.Assert(err, check.IsNil)
+
+ _, err = s.fs.OpenFile("/fg2/A Subproject", 0, 0)
+ c.Assert(err, check.IsNil)
+
+ _, err = s.fs.OpenFile("/fg2/A Project", 0, 0)
+ c.Assert(err, check.IsNil)
+
+ // An 'is_a' 'arvados#collection' filter means only collections should be returned.
+ s.fs.MountProject("fg3", fixtureAFilterGroupThreeUUID)
+
+ _, err = s.fs.OpenFile("/fg3/baz_file", 0, 0)
+ c.Assert(err, check.IsNil)
+
+ _, err = s.fs.OpenFile("/fg3/A Subproject", 0, 0)
+ c.Assert(err, check.Not(check.IsNil))
+}
+
func (s *SiteFSSuite) TestCurrentUserHome(c *check.C) {
s.fs.MountProject("home", "")
s.testHomeProject(c, "/home")
// package].
fixtureActiveToken = "3kg6k6lzmp9kj5cpkcoxie963cmvjahbt2fod9zru30k1jqdmi"
fixtureAProjectUUID = "zzzzz-j7d0g-v955i6s2oi1cbso"
+ fixtureThisFilterGroupUUID = "zzzzz-j7d0g-thisfiltergroup"
+ fixtureAFilterGroupTwoUUID = "zzzzz-j7d0g-afiltergrouptwo"
+ fixtureAFilterGroupThreeUUID = "zzzzz-j7d0g-filtergroupthre"
fixtureFooAndBarFilesInDirUUID = "zzzzz-4zz18-foonbarfilesdir"
fixtureFooCollectionName = "zzzzz-4zz18-fy296fx3hot09f7 added sometime"
fixtureFooCollectionPDH = "1f4b0bc7583c2a7f9102c395f4ffc5e3+45"
as.appendCall(ctx, as.GroupDelete, options)
return arvados.Group{}, as.Error
}
+func (as *APIStub) GroupTrash(ctx context.Context, options arvados.DeleteOptions) (arvados.Group, error) {
+ as.appendCall(ctx, as.GroupTrash, options)
+ return arvados.Group{}, as.Error
+}
func (as *APIStub) GroupUntrash(ctx context.Context, options arvados.UntrashOptions) (arvados.Group, error) {
as.appendCall(ctx, as.GroupUntrash, options)
return arvados.Group{}, as.Error
:self_link => "",
:offset => @offset,
:limit => @limit,
- :items_available => @items_available,
:items => @objects.as_api_response(nil)
}
+ if params[:count] != 'none'
+ list[:items_available] = @items_available
+ end
if @extra_included
list[:included] = @extra_included.as_api_response(nil, {select: @select})
end
seen_last_class = false
klasses.each do |klass|
- @offset = 0 if seen_last_class # reset offset for the new next type being processed
-
# if current klass is same as params['last_object_class'], mark that fact
seen_last_class = true if((params['count'].andand.==('none')) and
(params['last_object_class'].nil? or
if klass == Collection
@select = klass.selectable_attributes - ["manifest_text", "unsigned_manifest_text"]
elsif klass == Group
- where_conds = where_conds.merge(group_class: "project")
+ where_conds = where_conds.merge(group_class: ["project","filter"])
end
@filters = request_filters.map do |col, op, val|
if params['exclude_home_project']
@objects = exclude_home @objects, klass
end
+ if params['count'] == 'none'
+ # The call to object_list below will not populate :items_available in
+ # its response, because count is disabled. Save @objects length (does
+ # not require another db query) so that @offset (if set) is handled
+ # correctly.
+ countless_items_available = @objects.length
+ end
klass_limit = limit_all - all_objects.count
@limit = klass_limit
apply_where_limit_order_params klass
klass_object_list = object_list(model_class: klass)
- klass_items_available = klass_object_list[:items_available] || 0
+ if params['count'] != 'none'
+ klass_items_available = klass_object_list[:items_available] || 0
+ else
+ # klass_object_list[:items_available] is not populated
+ klass_items_available = countless_items_available
+ end
@items_available += klass_items_available
@offset = [@offset - klass_items_available, 0].max
all_objects += klass_object_list[:items]
validate :ensure_filesystem_compatible_name
validate :check_group_class
+ validate :check_filter_group_filters
before_create :assign_name
after_create :after_ownership_change
after_create :update_trash
end
end
+ def check_filter_group_filters
+ if group_class == 'filter'
+ if !self.properties.key?("filters")
+ errors.add :properties, "filters property missing, it must be an array of arrays, each with 3 elements"
+ return
+ end
+ if !self.properties["filters"].is_a?(Array)
+ errors.add :properties, "filters property must be an array of arrays, each with 3 elements"
+ return
+ end
+ self.properties["filters"].each do |filter|
+ if !filter.is_a?(Array)
+ errors.add :properties, "filters property must be an array of arrays, each with 3 elements"
+ return
+ end
+ if filter.length() != 3
+ errors.add :properties, "filters property must be an array of arrays, each with 3 elements"
+ return
+ end
+ if !filter[0].include?(".") and filter[0].downcase != "uuid"
+ errors.add :properties, "filter attribute must be 'uuid' or contain a dot (e.g. groups.name)"
+ return
+ end
+ if (filter[0].downcase != "uuid" and filter[1].downcase == "is_a")
+ errors.add :properties, "when filter operator is 'is_a', attribute must be 'uuid'"
+ return
+ end
+ if ! ["=","<","<=",">",">=","!=","like","ilike","in","not in","is_a"].include?(filter[1].downcase)
+ errors.add :properties, "filter operator is not valid (must be =,<,<=,>,>=,!=,like,ilike,in,not in,is_a)"
+ return
+ end
+ end
+ end
+ end
+
def update_trash
if saved_change_to_trash_at? or saved_change_to_owner_uuid?
# The group was added or removed from the trash.
# shouldn't be anything to do at all.
act_as_system_user do
ActiveRecord::Base.transaction do
- Group.where("group_class != 'project' or group_class is null").each do |g|
- # 1) any group not group_class != project becomes a 'role' (both empty and invalid groups)
+ Group.where("(group_class != 'project' and group_class != 'filter') or group_class is null").each do |g|
+ # 1) any group not group_class != project and != filter becomes a 'role' (both empty and invalid groups)
old_owner = g.owner_uuid
g.owner_uuid = system_user_uuid
g.group_class = 'role'
description: "Test project belonging to active user's first test project"
group_class: project
+afiltergroup:
+ uuid: zzzzz-j7d0g-thisfiltergroup
+ owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+ created_at: 2014-04-21 15:37:48 -0400
+ modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+ modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+ modified_at: 2014-04-21 15:37:48 -0400
+ updated_at: 2014-04-21 15:37:48 -0400
+ name: This filter group
+ group_class: filter
+ properties:
+ filters: [[ "collections.name", "like", "baz%" ], [ "groups.name", "=", "A Subproject" ]]
+
+afiltergroup2:
+ uuid: zzzzz-j7d0g-afiltergrouptwo
+ owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+ created_at: 2014-04-21 15:37:48 -0400
+ modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+ modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+ modified_at: 2014-04-21 15:37:48 -0400
+ updated_at: 2014-04-21 15:37:48 -0400
+ name: A filter group without filters
+ group_class: filter
+ properties:
+ filters: []
+
+afiltergroup3:
+ uuid: zzzzz-j7d0g-filtergroupthre
+ owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+ created_at: 2014-04-21 15:37:48 -0400
+ modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+ modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+ modified_at: 2014-04-21 15:37:48 -0400
+ updated_at: 2014-04-21 15:37:48 -0400
+ name: A filter group with an is_a collection filter
+ group_class: filter
+ properties:
+ filters: [["uuid", "is_a", "arvados#collection"]]
+
future_project_viewing_group:
uuid: zzzzz-j7d0g-futrprojviewgrp
owner_uuid: zzzzz-tpzed-000000000000000
refute(json_response.has_key?('items_available'))
end
+ test 'do not count items_available if count=none for group contents endpoint' do
+ @controller = Arvados::V1::GroupsController.new
+ authorize_with :active
+ get :contents, params: {
+ count: 'none',
+ }
+ assert_response(:success)
+ refute(json_response.has_key?('items_available'))
+ end
+
[{}, {count: nil}, {count: ''}, {count: 'exact'}].each do |params|
test "count items_available if params=#{params.inspect}" do
@controller = Arvados::V1::LinksController.new
end
assert_equal true, found_projects.include?(groups(:starred_and_shared_active_user_project).uuid)
end
+
+ test 'count none works with offset' do
+ first_results = nil
+ (0..10).each do |offset|
+ get "/arvados/v1/groups/contents", params: {
+ id: groups(:aproject).uuid,
+ offset: offset,
+ format: :json,
+ order: :uuid,
+ count: :none,
+ }, headers: auth(:active)
+ assert_response :success
+ assert_nil json_response['items_available']
+ if first_results.nil?
+ first_results = json_response['items']
+ else
+ assert_equal first_results[offset]['uuid'], json_response['items'][0]['uuid']
+ end
+ end
+ end
end
class NonTransactionalGroupsTest < ActionDispatch::IntegrationTest
Rails.configuration.Collections.ForwardSlashNameSubstitution = subst
proj = Group.create group_class: "project"
role = Group.create group_class: "role"
- filt = Group.create group_class: "filter"
+ filt = Group.create group_class: "filter", properties: {"filters":[]}
[[nil, true],
["", true],
[".", false],
role.name = name
assert_equal true, role.valid?
proj.name = name
- assert_equal valid, proj.valid?, "#{name.inspect} should be #{valid ? "valid" : "invalid"}"
+ assert_equal valid, proj.valid?, "project: #{name.inspect} should be #{valid ? "valid" : "invalid"}"
filt.name = name
- assert_equal valid, filt.valid?, "#{name.inspect} should be #{valid ? "valid" : "invalid"}"
+ assert_equal valid, filt.valid?, "filter: #{name.inspect} should be #{valid ? "valid" : "invalid"}"
end
end
end
if group_uuid_pattern.match(k):
project = self.api.groups().list(
- filters=[['group_class', '=', 'project'], ["uuid", "=", k]]).execute(num_retries=self.num_retries)
+ filters=[['group_class', 'in', ['project','filter']], ["uuid", "=", k]]).execute(num_retries=self.num_retries)
if project[u'items_available'] == 0:
return False
e = self.inodes.add_entry(ProjectDirectory(
"""A special directory that contains the contents of a project."""
def __init__(self, parent_inode, inodes, api, num_retries, project_object,
- poll=False, poll_time=60):
+ poll=True, poll_time=3):
super(ProjectDirectory, self).__init__(parent_inode, inodes, api.config)
self.api = api
self.num_retries = num_retries
self.num_retries,
uuid=self.project_uuid,
filters=[["uuid", "is_a", "arvados#group"],
- ["group_class", "=", "project"]])
+ ["groups.group_class", "in", ["project","filter"]]])
contents.extend(arvados.util.list_all(self.api.groups().contents,
self.num_retries,
uuid=self.project_uuid,
else:
namefilter = ["name", "in", [k, k2]]
contents = self.api.groups().list(filters=[["owner_uuid", "=", self.project_uuid],
- ["group_class", "=", "project"],
+ ["group_class", "in", ["project","filter"]],
namefilter],
limit=2).execute(num_retries=self.num_retries)["items"]
if not contents:
if 'httpMethod' in methods.get('shared', {}):
page = []
while True:
- resp = self.api.groups().shared(filters=[['group_class', '=', 'project']]+page,
+ resp = self.api.groups().shared(filters=[['group_class', 'in', ['project','filter']]]+page,
order="uuid",
limit=10000,
count="none",
else:
all_projects = arvados.util.list_all(
self.api.groups().list, self.num_retries,
- filters=[['group_class','=','project']],
+ filters=[['group_class','in',['project','filter']]],
select=["uuid", "owner_uuid"])
for ob in all_projects:
objects[ob['uuid']] = ob
self.test_project = run_test_server.fixture('groups')['aproject']['uuid']
self.non_project_group = run_test_server.fixture('groups')['public_role']['uuid']
+ self.filter_group = run_test_server.fixture('groups')['afiltergroup']['uuid']
self.collection_in_test_project = run_test_server.fixture('collections')['foo_collection_in_aproject']['name']
+ self.collection_in_filter_group = run_test_server.fixture('collections')['baz_file']['name']
cw = arvados.CollectionWriter()
llfuse.listdir(os.path.join(self.mounttmp, self.test_project)))
self.assertIn(self.collection_in_test_project,
llfuse.listdir(os.path.join(self.mounttmp, 'by_id', self.test_project)))
+ self.assertIn(self.collection_in_filter_group,
+ llfuse.listdir(os.path.join(self.mounttmp, self.filter_group)))
+ self.assertIn(self.collection_in_filter_group,
+ llfuse.listdir(os.path.join(self.mounttmp, 'by_id', self.filter_group)))
+
mount_ls = llfuse.listdir(self.mounttmp)
self.assertIn('README', mount_ls)
self.assertIn(self.test_project, mount_ls)
self.assertIn(self.test_project,
llfuse.listdir(os.path.join(self.mounttmp, 'by_id')))
+ self.assertIn(self.filter_group,
+ llfuse.listdir(os.path.join(self.mounttmp, 'by_id')))
with self.assertRaises(OSError):
llfuse.listdir(os.path.join(self.mounttmp, 'by_id', self.non_project_group))