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