5824: Merge branch 'master' into 5824-keep-web-workbench
[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 => 'Jobs_and_pipelines',
67         :filters => [%w(uuid is_a) + [%w(arvados#job 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           objects = @object.contents(order: @order,
217                                      limit: @limit,
218                                      filters: (@filters - kind_filters + [['uuid', 'is_a', type]]),
219                                     )
220           objects.each do |object|
221             @name_link_for[object.andand.uuid] = objects.links_for(object, 'name').first
222           end
223           @objects += objects
224         end
225       end
226       @objects = @objects.to_a.sort_by(&:created_at)
227       @objects.reverse! if nextpage_operator == '<='
228       @objects = @objects[0..@limit-1]
229       @next_page_filters = @filters.reject do |attr,op,val|
230         (attr == 'created_at' and op == nextpage_operator) or
231           (attr == 'uuid' and op == 'not in')
232       end
233
234       if @objects.any?
235         last_created_at = @objects.last.created_at
236
237         last_uuids = []
238         @objects.each do |obj|
239           last_uuids << obj.uuid if obj.created_at.eql?(last_created_at)
240         end
241
242         @next_page_filters += [['created_at',
243                                 nextpage_operator,
244                                 last_created_at]]
245         @next_page_filters += [['uuid', 'not in', last_uuids]]
246         @next_page_href = url_for(partial: :contents_rows,
247                                   limit: @limit,
248                                   filters: @next_page_filters.to_json)
249       else
250         @next_page_href = nil
251       end
252     else
253       @objects = @object.contents(order: @order,
254                                   limit: @limit,
255                                   filters: @filters,
256                                   offset: @offset)
257       @next_page_href = next_page_href(partial: :contents_rows,
258                                        filters: @filters.to_json,
259                                        order: @order.to_json)
260     end
261
262     preload_links_for_objects(@objects.to_a)
263   end
264
265   def show
266     if !@object
267       return render_not_found("object not found")
268     end
269
270     if params[:partial]
271       load_contents_objects
272       respond_to do |f|
273         f.json {
274           render json: {
275             content: render_to_string(partial: 'show_contents_rows.html',
276                                       formats: [:html]),
277             next_page_href: @next_page_href
278           }
279         }
280       end
281     else
282       @objects = []
283       super
284     end
285   end
286
287   def create
288     @new_resource_attrs = (params['project'] || {}).merge(group_class: 'project')
289     @new_resource_attrs[:name] ||= 'New project'
290     super
291   end
292
293   def update
294     @updates = params['project']
295     super
296   end
297
298   helper_method :get_objects_and_names
299   def get_objects_and_names(objects=nil)
300     objects = @objects if objects.nil?
301     objects_and_names = []
302     objects.each do |object|
303       if objects.respond_to? :links_for and
304           !(name_links = objects.links_for(object, 'name')).empty?
305         name_links.each do |name_link|
306           objects_and_names << [object, name_link]
307         end
308       elsif @name_link_for.andand[object.uuid]
309         objects_and_names << [object, @name_link_for[object.uuid]]
310       elsif object.respond_to? :name
311         objects_and_names << [object, object]
312       else
313         objects_and_names << [object,
314                                Link.new(owner_uuid: @object.uuid,
315                                         tail_uuid: @object.uuid,
316                                         head_uuid: object.uuid,
317                                         link_class: "name",
318                                         name: "")]
319
320       end
321     end
322     objects_and_names
323   end
324
325   def public  # Yes 'public' is the name of the action for public projects
326     return render_not_found if not Rails.configuration.anonymous_user_token or not Rails.configuration.enable_public_projects_page
327     @objects = using_specific_api_token Rails.configuration.anonymous_user_token do
328       Group.where(group_class: 'project').order("updated_at DESC")
329     end
330   end
331 end