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