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