4091: add offset to next page url
[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
209       # We are using created_at time slightly greater/lower than the last object created_at (see next block comment).
210       # This would mean that the server would now return the previous last item(s) with matching created_at again.
211       # Hence, we need to remove the previous last item (last_uuid) from results before displaying the rest of the
212       # results to prevent "infinite" infinite scrolling.
213       if params['last_uuid'] and @objects.any?
214         @objects.each do |obj|
215           @objects.delete obj if obj.uuid.eql?(params['last_uuid'])
216         end
217       end
218
219       if @objects.any?
220         last_created_at = @objects.last.created_at
221
222         # In order to prevent losing item(s) that have the same created_at time as the current page last item,
223         # next page should look for objects with created_at time slightly greater/lower than the current last.
224         if nextpage_operator == '<'
225           last_created_at += 1
226         else
227           last_created_at -= 1
228         end
229
230         @next_page_filters += [['created_at',
231                                 nextpage_operator,
232                                 last_created_at]]
233         @next_page_href = url_for(partial: :contents_rows,
234                                   last_uuid: @objects.last.uuid,
235                                   limit: @limit,
236                                   filters: @next_page_filters.to_json)
237       else
238         @next_page_href = nil
239       end
240     else
241       @objects = @object.contents(order: @order,
242                                   limit: @limit,
243                                   include_linked: true,
244                                   filters: @filters,
245                                   offset: @offset)
246       @next_page_href = next_page_href(partial: :contents_rows)
247     end
248
249     preload_links_for_objects(@objects.to_a)
250   end
251
252   def show
253     if !@object
254       return render_not_found("object not found")
255     end
256
257     if params[:partial]
258       load_contents_objects
259       respond_to do |f|
260         f.json {
261           render json: {
262             content: render_to_string(partial: 'show_contents_rows.html',
263                                       formats: [:html]),
264             next_page_href: @next_page_href
265           }
266         }
267       end
268     else
269       @objects = []
270       super
271     end
272   end
273
274   def create
275     @new_resource_attrs = (params['project'] || {}).merge(group_class: 'project')
276     @new_resource_attrs[:name] ||= 'New project'
277     super
278   end
279
280   def update
281     @updates = params['project']
282     super
283   end
284
285   helper_method :get_objects_and_names
286   def get_objects_and_names(objects=nil)
287     objects = @objects if objects.nil?
288     objects_and_names = []
289     objects.each do |object|
290       if objects.respond_to? :links_for and
291           !(name_links = objects.links_for(object, 'name')).empty?
292         name_links.each do |name_link|
293           objects_and_names << [object, name_link]
294         end
295       elsif @name_link_for.andand[object.uuid]
296         objects_and_names << [object, @name_link_for[object.uuid]]
297       elsif object.respond_to? :name
298         objects_and_names << [object, object]
299       else
300         objects_and_names << [object,
301                                Link.new(owner_uuid: @object.uuid,
302                                         tail_uuid: @object.uuid,
303                                         head_uuid: object.uuid,
304                                         link_class: "name",
305                                         name: "")]
306
307       end
308     end
309     objects_and_names
310   end
311
312   def share_with
313     if not params[:uuids].andand.any?
314       @errors = ["No user/group UUIDs specified to share with."]
315       return render_error(status: 422)
316     end
317     results = {"success" => [], "errors" => []}
318     params[:uuids].each do |shared_uuid|
319       begin
320         Link.create(tail_uuid: shared_uuid, link_class: "permission",
321                     name: "can_read", head_uuid: @object.uuid)
322       rescue ArvadosApiClient::ApiError => error
323         error_list = error.api_response.andand[:errors]
324         if error_list.andand.any?
325           results["errors"] += error_list.map { |e| "#{shared_uuid}: #{e}" }
326         else
327           error_code = error.api_status || "Bad status"
328           results["errors"] << "#{shared_uuid}: #{error_code} response"
329         end
330       else
331         results["success"] << shared_uuid
332       end
333     end
334     if results["errors"].empty?
335       results.delete("errors")
336       status = 200
337     else
338       status = 422
339     end
340     respond_to do |f|
341       f.json { render(json: results, status: status) }
342     end
343   end
344 end