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