Merge branch '17154-wb-profile' refs #17154
[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_action :set_share_links, if: -> { defined? @object and @object}
7   skip_around_action :require_thread_api_token, if: proc { |ctrl|
8     !Rails.configuration.Users.AnonymousUserToken.empty? 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     params[:item_uuids].collect { |uuid| ArvadosBase.find uuid }.each do |item|
136       if item.class == Collection or item.class == Group or item.class == Workflow or item.class == ContainerRequest
137         # Use delete API on collections and projects/groups
138         item.destroy
139         @removed_uuids << item.uuid
140       elsif item.owner_uuid == @object.uuid
141         # Object is owned by this project. Remove it from the project by
142         # changing owner to the current user.
143         begin
144           item.update_attributes owner_uuid: current_user.uuid
145           @removed_uuids << item.uuid
146         rescue ArvadosApiClient::ApiErrorResponseException => e
147           if e.message.include? '_owner_uuid_'
148             rename_to = item.name + ' removed from ' +
149                         (@object.name ? @object.name : @object.uuid) +
150                         ' at ' + Time.now.to_s
151             updates = {}
152             updates[:name] = rename_to
153             updates[:owner_uuid] = current_user.uuid
154             item.update_attributes updates
155             @removed_uuids << item.uuid
156           else
157             raise
158           end
159         end
160       end
161     end
162   end
163
164   def destroy
165     while (objects = Link.filter([['owner_uuid','=',@object.uuid],
166                                   ['tail_uuid','=',@object.uuid]]).with_count("none")).any?
167       objects.each do |object|
168         object.destroy
169       end
170     end
171     while (objects = @object.contents).any?
172       objects.each do |object|
173         object.update_attributes! owner_uuid: current_user.uuid
174       end
175     end
176     if ArvadosBase::resource_class_for_uuid(@object.owner_uuid) == Group
177       params[:return_to] ||= group_path(@object.owner_uuid)
178     else
179       params[:return_to] ||= projects_path
180     end
181     super
182   end
183
184   def find_objects_for_index
185     # We can use the all_projects helper, but we have to dup the
186     # result -- otherwise, when we apply our per-request filters and
187     # limits, they will infect the @all_projects cache too (see
188     # #6640).
189     @objects = all_projects.dup
190     super
191   end
192
193   def load_contents_objects kinds=[]
194     kind_filters = @filters.select do |attr,op,val|
195       op == 'is_a' and val.is_a? Array and val.count > 1
196     end
197     if /^created_at\b/ =~ @order[0] and kind_filters.count == 1
198       # If filtering on multiple types and sorting by date: Get the
199       # first page of each type, sort the entire set, truncate to one
200       # page, and use the last item on this page as a filter for
201       # retrieving the next page. Ideally the API would do this for
202       # us, but it doesn't (yet).
203
204       # To avoid losing items that have the same created_at as the
205       # last item on this page, we retrieve an overlapping page with a
206       # "created_at <= last_created_at" filter, then remove duplicates
207       # with a "uuid not in [...]" filter (see below).
208       nextpage_operator = /\bdesc$/i =~ @order[0] ? '<=' : '>='
209
210       @objects = []
211       @name_link_for = {}
212       kind_filters.each do |attr,op,val|
213         (val.is_a?(Array) ? val : [val]).each do |type|
214           klass = type.split('#')[-1]
215           klass[0] = klass[0].capitalize
216           next if(!Object.const_get(klass).api_exists?(:index))
217
218           filters = @filters - kind_filters + [['uuid', 'is_a', type]]
219           if type == 'arvados#containerRequest'
220             filters = filters + [['container_requests.requesting_container_uuid', '=', nil]]
221           end
222           objects = @object.contents(order: @order,
223                                      limit: @limit,
224                                      filters: filters,
225                                     )
226           objects.each do |object|
227             @name_link_for[object.andand.uuid] = objects.links_for(object, 'name').first
228           end
229           @objects += objects
230         end
231       end
232       @objects = @objects.to_a.sort_by(&:created_at)
233       @objects.reverse! if nextpage_operator == '<='
234       @objects = @objects[0..@limit-1]
235
236       if @objects.any?
237         @next_page_filters = next_page_filters(nextpage_operator)
238         @next_page_href = url_for(partial: :contents_rows,
239                                   limit: @limit,
240                                   filters: @next_page_filters.to_json)
241       else
242         @next_page_href = nil
243       end
244     else
245       @objects = @object.contents(order: @order,
246                                   limit: @limit,
247                                   filters: @filters,
248                                   offset: @offset)
249       @next_page_href = next_page_href(partial: :contents_rows,
250                                        filters: @filters.to_json,
251                                        order: @order.to_json)
252     end
253
254     preload_links_for_objects(@objects.to_a)
255   end
256
257   def show
258     if !@object
259       return render_not_found("object not found")
260     end
261
262     if params[:partial]
263       load_contents_objects
264       respond_to do |f|
265         f.json {
266           render json: {
267             content: render_to_string(partial: 'show_contents_rows.html',
268                                       formats: [:html]),
269             next_page_href: @next_page_href
270           }
271         }
272       end
273     else
274       @objects = []
275       super
276     end
277   end
278
279   def create
280     @new_resource_attrs = (params['project'] || {}).merge(group_class: 'project')
281     @new_resource_attrs[:name] ||= 'New project'
282     super
283   end
284
285   def update
286     @updates = params['project']
287     super
288   end
289
290   helper_method :get_objects_and_names
291   def get_objects_and_names(objects=nil)
292     objects = @objects if objects.nil?
293     objects_and_names = []
294     objects.each do |object|
295       if objects.respond_to? :links_for and
296           !(name_links = objects.links_for(object, 'name')).empty?
297         name_links.each do |name_link|
298           objects_and_names << [object, name_link]
299         end
300       elsif @name_link_for.andand[object.uuid]
301         objects_and_names << [object, @name_link_for[object.uuid]]
302       elsif object.respond_to? :name
303         objects_and_names << [object, object]
304       else
305         objects_and_names << [object,
306                                Link.new(owner_uuid: @object.uuid,
307                                         tail_uuid: @object.uuid,
308                                         head_uuid: object.uuid,
309                                         link_class: "name",
310                                         name: "")]
311
312       end
313     end
314     objects_and_names
315   end
316
317   def public  # Yes 'public' is the name of the action for public projects
318     return render_not_found if Rails.configuration.Users.AnonymousUserToken.empty? or not Rails.configuration.Workbench.EnablePublicProjectsPage
319     @objects = using_specific_api_token Rails.configuration.Users.AnonymousUserToken do
320       Group.where(group_class: 'project').order("modified_at DESC")
321     end
322   end
323 end