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