closes #5365
[arvados.git] / apps / workbench / app / controllers / projects_controller.rb
1 class ProjectsController < ApplicationController
2   before_filter :set_share_links, if: -> { defined? @object }
3   skip_around_filter :require_thread_api_token, if: proc { |ctrl|
4     Rails.configuration.anonymous_user_token and
5     %w(show tab_counts).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
14       @object = current_user.dup
15       @object.uuid = current_user.uuid
16       class << @object
17         def name
18           'Home'
19         end
20         def description
21           ''
22         end
23         def attribute_editable? attr, *args
24           case attr
25           when 'description', 'name'
26             false
27           else
28             super
29           end
30         end
31       end
32     else
33       super
34     end
35   end
36
37   def index_pane_list
38     %w(Projects)
39   end
40
41   # Returning an array of hashes instead of an array of strings will allow
42   # us to tell the interface to get counts for each pane (using :filters).
43   # It also seems to me that something like these could be used to configure the contents of the panes.
44   def show_pane_list
45     pane_list = []
46     if @object.uuid != current_user.andand.uuid
47       pane_list << 'Description'
48     end
49     pane_list <<
50       {
51         :name => 'Data_collections',
52         :filters => [%w(uuid is_a arvados#collection)]
53       }
54     pane_list <<
55       {
56         :name => 'Jobs_and_pipelines',
57         :filters => [%w(uuid is_a) + [%w(arvados#job arvados#pipelineInstance)]]
58       }
59     pane_list <<
60       {
61         :name => 'Pipeline_templates',
62         :filters => [%w(uuid is_a arvados#pipelineTemplate)]
63       }
64     pane_list <<
65       {
66         :name => 'Subprojects',
67         :filters => [%w(uuid is_a arvados#group)]
68       } if current_user
69     pane_list <<
70       {
71         :name => 'Other_objects',
72         :filters => [%w(uuid is_a) + [%w(arvados#human arvados#specimen arvados#trait)]]
73       } if current_user
74     pane_list << { :name => 'Sharing',
75                    :count => @share_links.count } if @user_is_manager
76     pane_list << { :name => 'Advanced' }
77   end
78
79   # Called via AJAX and returns Javascript that populates tab counts into tab titles.
80   # References #show_pane_list action which should return an array of hashes each with :name
81   # and then optionally a :filters to run or a straight up :count
82   #
83   # This action could easily be moved to the ApplicationController to genericize the tab_counts behaviour,
84   # but one or more new routes would have to be created, the js.erb would also have to be moved
85   def tab_counts
86     @tab_counts = {}
87     show_pane_list.each do |pane|
88       if pane.is_a?(Hash)
89         if pane[:count]
90           @tab_counts[pane[:name]] = pane[:count]
91         elsif pane[:filters]
92           @tab_counts[pane[:name]] = @object.contents(filters: pane[:filters]).items_available
93         end
94       end
95     end
96   end
97
98   def remove_item
99     params[:item_uuids] = [params[:item_uuid]]
100     remove_items
101     render template: 'projects/remove_items'
102   end
103
104   def remove_items
105     @removed_uuids = []
106     links = []
107     params[:item_uuids].collect { |uuid| ArvadosBase.find uuid }.each do |item|
108       if (item.class == Link and
109           item.link_class == 'name' and
110           item.tail_uuid == @object.uuid)
111         # Given uuid is a name link, linking an object to this
112         # project. First follow the link to find the item we're removing,
113         # then delete the link.
114         links << item
115         item = ArvadosBase.find item.head_uuid
116       else
117         # Given uuid is an object. Delete all names.
118         links += Link.where(tail_uuid: @object.uuid,
119                             head_uuid: item.uuid,
120                             link_class: 'name')
121       end
122       links.each do |link|
123         @removed_uuids << link.uuid
124         link.destroy
125       end
126
127       # If this object has the 'expires_at' attribute, then simply mark it
128       # expired.
129       if item.attributes.include?("expires_at")
130         item.update_attributes expires_at: Time.now
131         @removed_uuids << item.uuid
132       elsif item.owner_uuid == @object.uuid
133         # Object is owned by this project. Remove it from the project by
134         # changing owner to the current user.
135         begin
136           item.update_attributes owner_uuid: current_user.uuid
137           @removed_uuids << item.uuid
138         rescue ArvadosApiClient::ApiErrorResponseException => e
139           if e.message.include? 'collection_owner_uuid_name_unique'
140             rename_to = item.name + ' removed from ' +
141                         (@object.name ? @object.name : @object.uuid) +
142                         ' at ' + Time.now.to_s
143             updates = {}
144             updates[:name] = rename_to
145             updates[:owner_uuid] = current_user.uuid
146             item.update_attributes updates
147             @removed_uuids << item.uuid
148           else
149             raise
150           end
151         end
152       end
153     end
154   end
155
156   def destroy
157     while (objects = Link.filter([['owner_uuid','=',@object.uuid],
158                                   ['tail_uuid','=',@object.uuid]])).any?
159       objects.each do |object|
160         object.destroy
161       end
162     end
163     while (objects = @object.contents).any?
164       objects.each do |object|
165         object.update_attributes! owner_uuid: current_user.uuid
166       end
167     end
168     if ArvadosBase::resource_class_for_uuid(@object.owner_uuid) == Group
169       params[:return_to] ||= group_path(@object.owner_uuid)
170     else
171       params[:return_to] ||= projects_path
172     end
173     super
174   end
175
176   def find_objects_for_index
177     @objects = all_projects
178     super
179   end
180
181   def load_contents_objects kinds=[]
182     kind_filters = @filters.select do |attr,op,val|
183       op == 'is_a' and val.is_a? Array and val.count > 1
184     end
185     if /^created_at\b/ =~ @order[0] and kind_filters.count == 1
186       # If filtering on multiple types and sorting by date: Get the
187       # first page of each type, sort the entire set, truncate to one
188       # page, and use the last item on this page as a filter for
189       # retrieving the next page. Ideally the API would do this for
190       # us, but it doesn't (yet).
191
192       # To avoid losing items that have the same created_at as the
193       # last item on this page, we retrieve an overlapping page with a
194       # "created_at <= last_created_at" filter, then remove duplicates
195       # with a "uuid not in [...]" filter (see below).
196       nextpage_operator = /\bdesc$/i =~ @order[0] ? '<=' : '>='
197
198       @objects = []
199       @name_link_for = {}
200       kind_filters.each do |attr,op,val|
201         (val.is_a?(Array) ? val : [val]).each do |type|
202           objects = @object.contents(order: @order,
203                                      limit: @limit,
204                                      filters: (@filters - kind_filters + [['uuid', 'is_a', type]]),
205                                     )
206           objects.each do |object|
207             @name_link_for[object.andand.uuid] = objects.links_for(object, 'name').first
208           end
209           @objects += objects
210         end
211       end
212       @objects = @objects.to_a.sort_by(&:created_at)
213       @objects.reverse! if nextpage_operator == '<='
214       @objects = @objects[0..@limit-1]
215       @next_page_filters = @filters.reject do |attr,op,val|
216         (attr == 'created_at' and op == nextpage_operator) or
217           (attr == 'uuid' and op == 'not in')
218       end
219
220       if @objects.any?
221         last_created_at = @objects.last.created_at
222
223         last_uuids = []
224         @objects.each do |obj|
225           last_uuids << obj.uuid if obj.created_at.eql?(last_created_at)
226         end
227
228         @next_page_filters += [['created_at',
229                                 nextpage_operator,
230                                 last_created_at]]
231         @next_page_filters += [['uuid', 'not in', last_uuids]]
232         @next_page_href = url_for(partial: :contents_rows,
233                                   limit: @limit,
234                                   filters: @next_page_filters.to_json)
235       else
236         @next_page_href = nil
237       end
238     else
239       @objects = @object.contents(order: @order,
240                                   limit: @limit,
241                                   filters: @filters,
242                                   offset: @offset)
243       @next_page_href = next_page_href(partial: :contents_rows,
244                                        filters: @filters.to_json,
245                                        order: @order.to_json)
246     end
247
248     preload_links_for_objects(@objects.to_a)
249   end
250
251   def show
252     if !@object
253       return render_not_found("object not found")
254     end
255
256     if params[:partial]
257       load_contents_objects
258       respond_to do |f|
259         f.json {
260           render json: {
261             content: render_to_string(partial: 'show_contents_rows.html',
262                                       formats: [:html]),
263             next_page_href: @next_page_href
264           }
265         }
266       end
267     else
268       @objects = []
269       super
270     end
271   end
272
273   def create
274     @new_resource_attrs = (params['project'] || {}).merge(group_class: 'project')
275     @new_resource_attrs[:name] ||= 'New project'
276     super
277   end
278
279   def update
280     @updates = params['project']
281     super
282   end
283
284   helper_method :get_objects_and_names
285   def get_objects_and_names(objects=nil)
286     objects = @objects if objects.nil?
287     objects_and_names = []
288     objects.each do |object|
289       if objects.respond_to? :links_for and
290           !(name_links = objects.links_for(object, 'name')).empty?
291         name_links.each do |name_link|
292           objects_and_names << [object, name_link]
293         end
294       elsif @name_link_for.andand[object.uuid]
295         objects_and_names << [object, @name_link_for[object.uuid]]
296       elsif object.respond_to? :name
297         objects_and_names << [object, object]
298       else
299         objects_and_names << [object,
300                                Link.new(owner_uuid: @object.uuid,
301                                         tail_uuid: @object.uuid,
302                                         head_uuid: object.uuid,
303                                         link_class: "name",
304                                         name: "")]
305
306       end
307     end
308     objects_and_names
309   end
310 end