Add 'apps/arv-web/' from commit 'f9732ad8460d013c2f28363655d0d1b91894dca5'
[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
187       # To avoid losing items that have the same created_at as the
188       # last item on this page, we retrieve an overlapping page with a
189       # "created_at <= last_created_at" filter, then remove duplicates
190       # with a "uuid not in [...]" filter (see below).
191       nextpage_operator = /\bdesc$/i =~ @order[0] ? '<=' : '>='
192
193       @objects = []
194       @name_link_for = {}
195       kind_filters.each do |attr,op,val|
196         (val.is_a?(Array) ? val : [val]).each do |type|
197           objects = @object.contents(order: @order,
198                                      limit: @limit,
199                                      include_linked: true,
200                                      filters: (@filters - kind_filters + [['uuid', 'is_a', type]]),
201                                     )
202           objects.each do |object|
203             @name_link_for[object.andand.uuid] = objects.links_for(object, 'name').first
204           end
205           @objects += objects
206         end
207       end
208       @objects = @objects.to_a.sort_by(&:created_at)
209       @objects.reverse! if nextpage_operator == '<='
210       @objects = @objects[0..@limit-1]
211       @next_page_filters = @filters.reject do |attr,op,val|
212         (attr == 'created_at' and op == nextpage_operator) or
213           (attr == 'uuid' and op == 'not in')
214       end
215
216       if @objects.any?
217         last_created_at = @objects.last.created_at
218
219         last_uuids = []
220         @objects.each do |obj|
221           last_uuids << obj.uuid if obj.created_at.eql?(last_created_at)
222         end
223
224         @next_page_filters += [['created_at',
225                                 nextpage_operator,
226                                 last_created_at]]
227         @next_page_filters += [['uuid', 'not in', last_uuids]]
228         @next_page_href = url_for(partial: :contents_rows,
229                                   limit: @limit,
230                                   filters: @next_page_filters.to_json)
231       else
232         @next_page_href = nil
233       end
234     else
235       @objects = @object.contents(order: @order,
236                                   limit: @limit,
237                                   include_linked: true,
238                                   filters: @filters,
239                                   offset: @offset)
240       @next_page_href = next_page_href(partial: :contents_rows,
241                                        filters: @filters.to_json,
242                                        order: @order.to_json)
243     end
244
245     preload_links_for_objects(@objects.to_a)
246   end
247
248   def show
249     if !@object
250       return render_not_found("object not found")
251     end
252
253     if params[:partial]
254       load_contents_objects
255       respond_to do |f|
256         f.json {
257           render json: {
258             content: render_to_string(partial: 'show_contents_rows.html',
259                                       formats: [:html]),
260             next_page_href: @next_page_href
261           }
262         }
263       end
264     else
265       @objects = []
266       super
267     end
268   end
269
270   def create
271     @new_resource_attrs = (params['project'] || {}).merge(group_class: 'project')
272     @new_resource_attrs[:name] ||= 'New project'
273     super
274   end
275
276   def update
277     @updates = params['project']
278     super
279   end
280
281   helper_method :get_objects_and_names
282   def get_objects_and_names(objects=nil)
283     objects = @objects if objects.nil?
284     objects_and_names = []
285     objects.each do |object|
286       if objects.respond_to? :links_for and
287           !(name_links = objects.links_for(object, 'name')).empty?
288         name_links.each do |name_link|
289           objects_and_names << [object, name_link]
290         end
291       elsif @name_link_for.andand[object.uuid]
292         objects_and_names << [object, @name_link_for[object.uuid]]
293       elsif object.respond_to? :name
294         objects_and_names << [object, object]
295       else
296         objects_and_names << [object,
297                                Link.new(owner_uuid: @object.uuid,
298                                         tail_uuid: @object.uuid,
299                                         head_uuid: object.uuid,
300                                         link_class: "name",
301                                         name: "")]
302
303       end
304     end
305     objects_and_names
306   end
307
308   def share_with
309     if not params[:uuids].andand.any?
310       @errors = ["No user/group UUIDs specified to share with."]
311       return render_error(status: 422)
312     end
313     results = {"success" => [], "errors" => []}
314     params[:uuids].each do |shared_uuid|
315       begin
316         Link.create(tail_uuid: shared_uuid, link_class: "permission",
317                     name: "can_read", head_uuid: @object.uuid)
318       rescue ArvadosApiClient::ApiError => error
319         error_list = error.api_response.andand[:errors]
320         if error_list.andand.any?
321           results["errors"] += error_list.map { |e| "#{shared_uuid}: #{e}" }
322         else
323           error_code = error.api_status || "Bad status"
324           results["errors"] << "#{shared_uuid}: #{error_code} response"
325         end
326       else
327         results["success"] << shared_uuid
328       end
329     end
330     if results["errors"].empty?
331       results.delete("errors")
332       status = 200
333     else
334       status = 422
335     end
336     respond_to do |f|
337       f.json { render(json: results, status: status) }
338     end
339   end
340 end