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