Merge branch 'master' into 3836-remove-collection-from-project-bug
[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           end
144         end
145       end
146     end
147   end
148
149   def destroy
150     while (objects = Link.filter([['owner_uuid','=',@object.uuid],
151                                   ['tail_uuid','=',@object.uuid]])).any?
152       objects.each do |object|
153         object.destroy
154       end
155     end
156     while (objects = @object.contents(include_linked: false)).any?
157       objects.each do |object|
158         object.update_attributes! owner_uuid: current_user.uuid
159       end
160     end
161     if ArvadosBase::resource_class_for_uuid(@object.owner_uuid) == Group
162       params[:return_to] ||= group_path(@object.owner_uuid)
163     else
164       params[:return_to] ||= projects_path
165     end
166     super
167   end
168
169   def find_objects_for_index
170     @objects = all_projects
171     super
172   end
173
174   def load_contents_objects kinds=[]
175     kind_filters = @filters.select do |attr,op,val|
176       op == 'is_a' and val.is_a? Array and val.count > 1
177     end
178     if /^created_at\b/ =~ @order[0] and kind_filters.count == 1
179       # If filtering on multiple types and sorting by date: Get the
180       # first page of each type, sort the entire set, truncate to one
181       # page, and use the last item on this page as a filter for
182       # retrieving the next page. Ideally the API would do this for
183       # us, but it doesn't (yet).
184       nextpage_operator = /\bdesc$/i =~ @order[0] ? '<' : '>'
185       @objects = []
186       @name_link_for = {}
187       kind_filters.each do |attr,op,val|
188         (val.is_a?(Array) ? val : [val]).each do |type|
189           objects = @object.contents(order: @order,
190                                      limit: @limit,
191                                      include_linked: true,
192                                      filters: (@filters - kind_filters + [['uuid', 'is_a', type]]),
193                                      offset: @offset)
194           objects.each do |object|
195             @name_link_for[object.andand.uuid] = objects.links_for(object, 'name').first
196           end
197           @objects += objects
198         end
199       end
200       @objects = @objects.to_a.sort_by(&:created_at)
201       @objects.reverse! if nextpage_operator == '<'
202       @objects = @objects[0..@limit-1]
203       @next_page_filters = @filters.reject do |attr,op,val|
204         attr == 'created_at' and op == nextpage_operator
205       end
206       if @objects.any?
207         @next_page_filters += [['created_at',
208                                 nextpage_operator,
209                                 @objects.last.created_at]]
210         @next_page_href = url_for(partial: :contents_rows,
211                                   filters: @next_page_filters.to_json)
212       else
213         @next_page_href = nil
214       end
215     else
216       @objects = @object.contents(order: @order,
217                                   limit: @limit,
218                                   include_linked: true,
219                                   filters: @filters,
220                                   offset: @offset)
221       @next_page_href = next_page_href(partial: :contents_rows)
222     end
223
224     preload_links_for_objects(@objects.to_a)
225   end
226
227   def show
228     if !@object
229       return render_not_found("object not found")
230     end
231
232     if params[:partial]
233       load_contents_objects
234       respond_to do |f|
235         f.json {
236           render json: {
237             content: render_to_string(partial: 'show_contents_rows.html',
238                                       formats: [:html]),
239             next_page_href: @next_page_href
240           }
241         }
242       end
243     else
244       @objects = []
245       super
246     end
247   end
248
249   def create
250     @new_resource_attrs = (params['project'] || {}).merge(group_class: 'project')
251     @new_resource_attrs[:name] ||= 'New project'
252     super
253   end
254
255   def update
256     @updates = params['project']
257     super
258   end
259
260   helper_method :get_objects_and_names
261   def get_objects_and_names(objects=nil)
262     objects = @objects if objects.nil?
263     objects_and_names = []
264     objects.each do |object|
265       if objects.respond_to? :links_for and
266           !(name_links = objects.links_for(object, 'name')).empty?
267         name_links.each do |name_link|
268           objects_and_names << [object, name_link]
269         end
270       elsif @name_link_for.andand[object.uuid]
271         objects_and_names << [object, @name_link_for[object.uuid]]
272       elsif object.respond_to? :name
273         objects_and_names << [object, object]
274       else
275         objects_and_names << [object,
276                                Link.new(owner_uuid: @object.uuid,
277                                         tail_uuid: @object.uuid,
278                                         head_uuid: object.uuid,
279                                         link_class: "name",
280                                         name: "")]
281
282       end
283     end
284     objects_and_names
285   end
286
287   def share_with
288     if not params[:uuids].andand.any?
289       @errors = ["No user/group UUIDs specified to share with."]
290       return render_error(status: 422)
291     end
292     results = {"success" => [], "errors" => []}
293     params[:uuids].each do |shared_uuid|
294       begin
295         Link.create(tail_uuid: shared_uuid, link_class: "permission",
296                     name: "can_read", head_uuid: @object.uuid)
297       rescue ArvadosApiClient::ApiError => error
298         error_list = error.api_response.andand[:errors]
299         if error_list.andand.any?
300           results["errors"] += error_list.map { |e| "#{shared_uuid}: #{e}" }
301         else
302           error_code = error.api_status || "Bad status"
303           results["errors"] << "#{shared_uuid}: #{error_code} response"
304         end
305       else
306         results["success"] << shared_uuid
307       end
308     end
309     if results["errors"].empty?
310       results.delete("errors")
311       status = 200
312     else
313       status = 422
314     end
315     respond_to do |f|
316       f.json { render(json: results, status: status) }
317     end
318   end
319 end