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