Merge branch '9850-cwl-js' closes #9850
[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) + [%w(arvados#pipelineTemplate arvados#workflow)]]
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
234       if @objects.any?
235         @next_page_filters = next_page_filters(nextpage_operator)
236         @next_page_href = url_for(partial: :contents_rows,
237                                   limit: @limit,
238                                   filters: @next_page_filters.to_json)
239       else
240         @next_page_href = nil
241       end
242     else
243       @objects = @object.contents(order: @order,
244                                   limit: @limit,
245                                   filters: @filters,
246                                   offset: @offset)
247       @next_page_href = next_page_href(partial: :contents_rows,
248                                        filters: @filters.to_json,
249                                        order: @order.to_json)
250     end
251
252     preload_links_for_objects(@objects.to_a)
253   end
254
255   def show
256     if !@object
257       return render_not_found("object not found")
258     end
259
260     if params[:partial]
261       load_contents_objects
262       respond_to do |f|
263         f.json {
264           render json: {
265             content: render_to_string(partial: 'show_contents_rows.html',
266                                       formats: [:html]),
267             next_page_href: @next_page_href
268           }
269         }
270       end
271     else
272       @objects = []
273       super
274     end
275   end
276
277   def create
278     @new_resource_attrs = (params['project'] || {}).merge(group_class: 'project')
279     @new_resource_attrs[:name] ||= 'New project'
280     super
281   end
282
283   def update
284     @updates = params['project']
285     super
286   end
287
288   helper_method :get_objects_and_names
289   def get_objects_and_names(objects=nil)
290     objects = @objects if objects.nil?
291     objects_and_names = []
292     objects.each do |object|
293       if objects.respond_to? :links_for and
294           !(name_links = objects.links_for(object, 'name')).empty?
295         name_links.each do |name_link|
296           objects_and_names << [object, name_link]
297         end
298       elsif @name_link_for.andand[object.uuid]
299         objects_and_names << [object, @name_link_for[object.uuid]]
300       elsif object.respond_to? :name
301         objects_and_names << [object, object]
302       else
303         objects_and_names << [object,
304                                Link.new(owner_uuid: @object.uuid,
305                                         tail_uuid: @object.uuid,
306                                         head_uuid: object.uuid,
307                                         link_class: "name",
308                                         name: "")]
309
310       end
311     end
312     objects_and_names
313   end
314
315   def public  # Yes 'public' is the name of the action for public projects
316     return render_not_found if not Rails.configuration.anonymous_user_token or not Rails.configuration.enable_public_projects_page
317     @objects = using_specific_api_token Rails.configuration.anonymous_user_token do
318       Group.where(group_class: 'project').order("updated_at DESC")
319     end
320   end
321 end