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