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