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