closes #4285
[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     end
225
226     preload_links_for_objects(@objects.to_a)
227   end
228
229   def show
230     if !@object
231       return render_not_found("object not found")
232     end
233
234     if params[:partial]
235       load_contents_objects
236       respond_to do |f|
237         f.json {
238           render json: {
239             content: render_to_string(partial: 'show_contents_rows.html',
240                                       formats: [:html]),
241             next_page_href: @next_page_href
242           }
243         }
244       end
245     else
246       @objects = []
247       super
248     end
249   end
250
251   def create
252     @new_resource_attrs = (params['project'] || {}).merge(group_class: 'project')
253     @new_resource_attrs[:name] ||= 'New project'
254     super
255   end
256
257   def update
258     @updates = params['project']
259     super
260   end
261
262   helper_method :get_objects_and_names
263   def get_objects_and_names(objects=nil)
264     objects = @objects if objects.nil?
265     objects_and_names = []
266     objects.each do |object|
267       if objects.respond_to? :links_for and
268           !(name_links = objects.links_for(object, 'name')).empty?
269         name_links.each do |name_link|
270           objects_and_names << [object, name_link]
271         end
272       elsif @name_link_for.andand[object.uuid]
273         objects_and_names << [object, @name_link_for[object.uuid]]
274       elsif object.respond_to? :name
275         objects_and_names << [object, object]
276       else
277         objects_and_names << [object,
278                                Link.new(owner_uuid: @object.uuid,
279                                         tail_uuid: @object.uuid,
280                                         head_uuid: object.uuid,
281                                         link_class: "name",
282                                         name: "")]
283
284       end
285     end
286     objects_and_names
287   end
288
289   def share_with
290     if not params[:uuids].andand.any?
291       @errors = ["No user/group UUIDs specified to share with."]
292       return render_error(status: 422)
293     end
294     results = {"success" => [], "errors" => []}
295     params[:uuids].each do |shared_uuid|
296       begin
297         Link.create(tail_uuid: shared_uuid, link_class: "permission",
298                     name: "can_read", head_uuid: @object.uuid)
299       rescue ArvadosApiClient::ApiError => error
300         error_list = error.api_response.andand[:errors]
301         if error_list.andand.any?
302           results["errors"] += error_list.map { |e| "#{shared_uuid}: #{e}" }
303         else
304           error_code = error.api_status || "Bad status"
305           results["errors"] << "#{shared_uuid}: #{error_code} response"
306         end
307       else
308         results["success"] << shared_uuid
309       end
310     end
311     if results["errors"].empty?
312       results.delete("errors")
313       status = 200
314     else
315       status = 422
316     end
317     respond_to do |f|
318       f.json { render(json: results, status: status) }
319     end
320   end
321 end