Merge branch 'master' into 2659-anonymous-share-projects
[arvados.git] / apps / workbench / app / controllers / projects_controller.rb
1 class ProjectsController < ApplicationController
2   before_filter :set_share_links, if: -> { defined? @object }
3   if Rails.configuration.anonymous_user_token
4     skip_around_filter :require_thread_api_token, only: [:show, :tab_counts]
5   end
6
7   def model_class
8     Group
9   end
10
11   def find_object_by_uuid
12     if current_user and params[:uuid] == current_user.uuid
13       @object = current_user.dup
14       @object.uuid = current_user.uuid
15       class << @object
16         def name
17           'Home'
18         end
19         def description
20           ''
21         end
22         def attribute_editable? attr, *args
23           case attr
24           when 'description', 'name'
25             false
26           else
27             super
28           end
29         end
30       end
31     else
32       super
33     end
34   end
35
36   def index_pane_list
37     %w(Projects)
38   end
39
40   # Returning an array of hashes instead of an array of strings will allow
41   # us to tell the interface to get counts for each pane (using :filters).
42   # It also seems to me that something like these could be used to configure the contents of the panes.
43   def show_pane_list
44     pane_list = []
45     pane_list <<
46       {
47         :name => 'Data_collections',
48         :filters => [%w(uuid is_a arvados#collection)]
49       }
50     pane_list <<
51       {
52         :name => 'Jobs_and_pipelines',
53         :filters => [%w(uuid is_a) + [%w(arvados#job arvados#pipelineInstance)]]
54       }
55     pane_list <<
56       {
57         :name => 'Pipeline_templates',
58         :filters => [%w(uuid is_a arvados#pipelineTemplate)]
59       }
60     pane_list <<
61       {
62         :name => 'Subprojects',
63         :filters => [%w(uuid is_a arvados#group)]
64       } if current_user
65     pane_list <<
66       {
67         :name => 'Other_objects',
68         :filters => [%w(uuid is_a) + [%w(arvados#human arvados#specimen arvados#trait)]]
69       } if current_user
70     pane_list << { :name => 'Sharing',
71                    :count => @share_links.count } if @user_is_manager
72     pane_list << { :name => 'Advanced' }
73   end
74
75   # Called via AJAX and returns Javascript that populates tab counts into tab titles.
76   # References #show_pane_list action which should return an array of hashes each with :name
77   # and then optionally a :filters to run or a straight up :count
78   #
79   # This action could easily be moved to the ApplicationController to genericize the tab_counts behaviour,
80   # but one or more new routes would have to be created, the js.erb would also have to be moved
81   def tab_counts
82     @tab_counts = {}
83     show_pane_list.each do |pane|
84       if pane.is_a?(Hash)
85         if pane[:count]
86           @tab_counts[pane[:name]] = pane[:count]
87         elsif pane[:filters]
88           @tab_counts[pane[:name]] = @object.contents(filters: pane[:filters]).items_available
89         end
90       end
91     end
92   end
93
94   def remove_item
95     params[:item_uuids] = [params[:item_uuid]]
96     remove_items
97     render template: 'projects/remove_items'
98   end
99
100   def remove_items
101     @removed_uuids = []
102     links = []
103     params[:item_uuids].collect { |uuid| ArvadosBase.find uuid }.each do |item|
104       if (item.class == Link and
105           item.link_class == 'name' and
106           item.tail_uuid == @object.uuid)
107         # Given uuid is a name link, linking an object to this
108         # project. First follow the link to find the item we're removing,
109         # then delete the link.
110         links << item
111         item = ArvadosBase.find item.head_uuid
112       else
113         # Given uuid is an object. Delete all names.
114         links += Link.where(tail_uuid: @object.uuid,
115                             head_uuid: item.uuid,
116                             link_class: 'name')
117       end
118       links.each do |link|
119         @removed_uuids << link.uuid
120         link.destroy
121       end
122
123       # If this object has the 'expires_at' attribute, then simply mark it
124       # expired.
125       if item.attributes.include?("expires_at")
126         item.update_attributes expires_at: Time.now
127         @removed_uuids << item.uuid
128       elsif item.owner_uuid == @object.uuid
129         # Object is owned by this project. Remove it from the project by
130         # changing owner to the current user.
131         begin
132           item.update_attributes owner_uuid: current_user.uuid
133           @removed_uuids << item.uuid
134         rescue ArvadosApiClient::ApiErrorResponseException => e
135           if e.message.include? 'collection_owner_uuid_name_unique'
136             rename_to = item.name + ' removed from ' +
137                         (@object.name ? @object.name : @object.uuid) +
138                         ' at ' + Time.now.to_s
139             updates = {}
140             updates[:name] = rename_to
141             updates[:owner_uuid] = current_user.uuid
142             item.update_attributes updates
143             @removed_uuids << item.uuid
144           else
145             raise
146           end
147         end
148       end
149     end
150   end
151
152   def destroy
153     while (objects = Link.filter([['owner_uuid','=',@object.uuid],
154                                   ['tail_uuid','=',@object.uuid]])).any?
155       objects.each do |object|
156         object.destroy
157       end
158     end
159     while (objects = @object.contents(include_linked: false)).any?
160       objects.each do |object|
161         object.update_attributes! owner_uuid: current_user.uuid
162       end
163     end
164     if ArvadosBase::resource_class_for_uuid(@object.owner_uuid) == Group
165       params[:return_to] ||= group_path(@object.owner_uuid)
166     else
167       params[:return_to] ||= projects_path
168     end
169     super
170   end
171
172   def find_objects_for_index
173     @objects = all_projects
174     super
175   end
176
177   def load_contents_objects kinds=[]
178     kind_filters = @filters.select do |attr,op,val|
179       op == 'is_a' and val.is_a? Array and val.count > 1
180     end
181     if /^created_at\b/ =~ @order[0] and kind_filters.count == 1
182       # If filtering on multiple types and sorting by date: Get the
183       # first page of each type, sort the entire set, truncate to one
184       # page, and use the last item on this page as a filter for
185       # retrieving the next page. Ideally the API would do this for
186       # us, but it doesn't (yet).
187
188       # To avoid losing items that have the same created_at as the
189       # last item on this page, we retrieve an overlapping page with a
190       # "created_at <= last_created_at" filter, then remove duplicates
191       # with a "uuid not in [...]" filter (see below).
192       nextpage_operator = /\bdesc$/i =~ @order[0] ? '<=' : '>='
193
194       @objects = []
195       @name_link_for = {}
196       kind_filters.each do |attr,op,val|
197         (val.is_a?(Array) ? val : [val]).each do |type|
198           objects = @object.contents(order: @order,
199                                      limit: @limit,
200                                      include_linked: true,
201                                      filters: (@filters - kind_filters + [['uuid', 'is_a', type]]),
202                                     )
203           objects.each do |object|
204             @name_link_for[object.andand.uuid] = objects.links_for(object, 'name').first
205           end
206           @objects += objects
207         end
208       end
209       @objects = @objects.to_a.sort_by(&:created_at)
210       @objects.reverse! if nextpage_operator == '<='
211       @objects = @objects[0..@limit-1]
212       @next_page_filters = @filters.reject do |attr,op,val|
213         (attr == 'created_at' and op == nextpage_operator) or
214           (attr == 'uuid' and op == 'not in')
215       end
216
217       if @objects.any?
218         last_created_at = @objects.last.created_at
219
220         last_uuids = []
221         @objects.each do |obj|
222           last_uuids << obj.uuid if obj.created_at.eql?(last_created_at)
223         end
224
225         @next_page_filters += [['created_at',
226                                 nextpage_operator,
227                                 last_created_at]]
228         @next_page_filters += [['uuid', 'not in', last_uuids]]
229         @next_page_href = url_for(partial: :contents_rows,
230                                   limit: @limit,
231                                   filters: @next_page_filters.to_json)
232       else
233         @next_page_href = nil
234       end
235     else
236       @objects = @object.contents(order: @order,
237                                   limit: @limit,
238                                   include_linked: true,
239                                   filters: @filters,
240                                   offset: @offset)
241       @next_page_href = next_page_href(partial: :contents_rows,
242                                        filters: @filters.to_json,
243                                        order: @order.to_json)
244     end
245
246     preload_links_for_objects(@objects.to_a)
247   end
248
249   def show
250     if !@object
251       return render_not_found("object not found")
252     end
253
254     if params[:partial]
255       load_contents_objects
256       respond_to do |f|
257         f.json {
258           render json: {
259             content: render_to_string(partial: 'show_contents_rows.html',
260                                       formats: [:html]),
261             next_page_href: @next_page_href
262           }
263         }
264       end
265     else
266       @objects = []
267       super
268     end
269   end
270
271   def create
272     @new_resource_attrs = (params['project'] || {}).merge(group_class: 'project')
273     @new_resource_attrs[:name] ||= 'New project'
274     super
275   end
276
277   def update
278     @updates = params['project']
279     super
280   end
281
282   helper_method :get_objects_and_names
283   def get_objects_and_names(objects=nil)
284     objects = @objects if objects.nil?
285     objects_and_names = []
286     objects.each do |object|
287       if objects.respond_to? :links_for and
288           !(name_links = objects.links_for(object, 'name')).empty?
289         name_links.each do |name_link|
290           objects_and_names << [object, name_link]
291         end
292       elsif @name_link_for.andand[object.uuid]
293         objects_and_names << [object, @name_link_for[object.uuid]]
294       elsif object.respond_to? :name
295         objects_and_names << [object, object]
296       else
297         objects_and_names << [object,
298                                Link.new(owner_uuid: @object.uuid,
299                                         tail_uuid: @object.uuid,
300                                         head_uuid: object.uuid,
301                                         link_class: "name",
302                                         name: "")]
303
304       end
305     end
306     objects_and_names
307   end
308 end