82743b90952f96240c4f11d48f4e6015bf1fff68
[arvados.git] / apps / workbench / app / controllers / projects_controller.rb
1 class ProjectsController < ApplicationController
2   before_filter :set_share_links, if: -> { defined? @object }
3   skip_around_filter :require_thread_api_token, only: :show
4
5   def model_class
6     Group
7   end
8
9   def find_object_by_uuid
10     if current_user and params[:uuid] == current_user.uuid
11       @object = current_user.dup
12       @object.uuid = current_user.uuid
13       class << @object
14         def name
15           'Home'
16         end
17         def description
18           ''
19         end
20         def attribute_editable? attr, *args
21           case attr
22           when 'description', 'name'
23             false
24           else
25             super
26           end
27         end
28       end
29     else
30       super
31     end
32   end
33
34   def index_pane_list
35     %w(Projects)
36   end
37
38   # Returning an array of hashes instead of an array of strings will allow
39   # us to tell the interface to get counts for each pane (using :filters).
40   # It also seems to me that something like these could be used to configure the contents of the panes.
41   def show_pane_list
42     pane_list = []
43     pane_list <<
44       {
45         :name => 'Data_collections',
46         :filters => [%w(uuid is_a arvados#collection)]
47       }
48     pane_list <<
49       {
50         :name => 'Jobs_and_pipelines',
51         :filters => [%w(uuid is_a) + [%w(arvados#job arvados#pipelineInstance)]]
52       }
53     pane_list <<
54       {
55         :name => 'Pipeline_templates',
56         :filters => [%w(uuid is_a arvados#pipelineTemplate)]
57       }
58     pane_list <<
59       {
60         :name => 'Subprojects',
61         :filters => [%w(uuid is_a arvados#group)]
62       } if current_user
63     pane_list <<
64       {
65         :name => 'Other_objects',
66         :filters => [%w(uuid is_a) + [%w(arvados#human arvados#specimen arvados#trait)]]
67       } if current_user
68     pane_list << { :name => 'Sharing',
69                    :count => @share_links.count } if @user_is_manager
70     pane_list << { :name => 'Advanced' }
71   end
72
73   # Called via AJAX and returns Javascript that populates tab counts into tab titles.
74   # References #show_pane_list action which should return an array of hashes each with :name
75   # and then optionally a :filters to run or a straight up :count
76   #
77   # This action could easily be moved to the ApplicationController to genericize the tab_counts behaviour,
78   # but one or more new routes would have to be created, the js.erb would also have to be moved
79   def tab_counts
80     @tab_counts = {}
81     show_pane_list.each do |pane|
82       if pane.is_a?(Hash)
83         if pane[:count]
84           @tab_counts[pane[:name]] = pane[:count]
85         elsif pane[:filters]
86           @tab_counts[pane[:name]] = @object.contents(filters: pane[:filters]).items_available
87         end
88       end
89     end
90   end
91
92   def remove_item
93     params[:item_uuids] = [params[:item_uuid]]
94     remove_items
95     render template: 'projects/remove_items'
96   end
97
98   def remove_items
99     @removed_uuids = []
100     links = []
101     params[:item_uuids].collect { |uuid| ArvadosBase.find uuid }.each do |item|
102       if (item.class == Link and
103           item.link_class == 'name' and
104           item.tail_uuid == @object.uuid)
105         # Given uuid is a name link, linking an object to this
106         # project. First follow the link to find the item we're removing,
107         # then delete the link.
108         links << item
109         item = ArvadosBase.find item.head_uuid
110       else
111         # Given uuid is an object. Delete all names.
112         links += Link.where(tail_uuid: @object.uuid,
113                             head_uuid: item.uuid,
114                             link_class: 'name')
115       end
116       links.each do |link|
117         @removed_uuids << link.uuid
118         link.destroy
119       end
120
121       # If this object has the 'expires_at' attribute, then simply mark it
122       # expired.
123       if item.attributes.include?("expires_at")
124         item.update_attributes expires_at: Time.now
125         @removed_uuids << item.uuid
126       elsif item.owner_uuid == @object.uuid
127         # Object is owned by this project. Remove it from the project by
128         # changing owner to the current user.
129         begin
130           item.update_attributes owner_uuid: current_user.uuid
131           @removed_uuids << item.uuid
132         rescue ArvadosApiClient::ApiErrorResponseException => e
133           if e.message.include? 'collection_owner_uuid_name_unique'
134             rename_to = item.name + ' removed from ' +
135                         (@object.name ? @object.name : @object.uuid) +
136                         ' at ' + Time.now.to_s
137             updates = {}
138             updates[:name] = rename_to
139             updates[:owner_uuid] = current_user.uuid
140             item.update_attributes updates
141             @removed_uuids << item.uuid
142           else
143             raise
144           end
145         end
146       end
147     end
148   end
149
150   def destroy
151     while (objects = Link.filter([['owner_uuid','=',@object.uuid],
152                                   ['tail_uuid','=',@object.uuid]])).any?
153       objects.each do |object|
154         object.destroy
155       end
156     end
157     while (objects = @object.contents(include_linked: false)).any?
158       objects.each do |object|
159         object.update_attributes! owner_uuid: current_user.uuid
160       end
161     end
162     if ArvadosBase::resource_class_for_uuid(@object.owner_uuid) == Group
163       params[:return_to] ||= group_path(@object.owner_uuid)
164     else
165       params[:return_to] ||= projects_path
166     end
167     super
168   end
169
170   def find_objects_for_index
171     @objects = all_projects
172     super
173   end
174
175   def load_contents_objects kinds=[]
176     kind_filters = @filters.select do |attr,op,val|
177       op == 'is_a' and val.is_a? Array and val.count > 1
178     end
179     if /^created_at\b/ =~ @order[0] and kind_filters.count == 1
180       # If filtering on multiple types and sorting by date: Get the
181       # first page of each type, sort the entire set, truncate to one
182       # page, and use the last item on this page as a filter for
183       # retrieving the next page. Ideally the API would do this for
184       # us, but it doesn't (yet).
185
186       # To avoid losing items that have the same created_at as the
187       # last item on this page, we retrieve an overlapping page with a
188       # "created_at <= last_created_at" filter, then remove duplicates
189       # with a "uuid not in [...]" filter (see below).
190       nextpage_operator = /\bdesc$/i =~ @order[0] ? '<=' : '>='
191
192       @objects = []
193       @name_link_for = {}
194       kind_filters.each do |attr,op,val|
195         (val.is_a?(Array) ? val : [val]).each do |type|
196           objects = @object.contents(order: @order,
197                                      limit: @limit,
198                                      include_linked: true,
199                                      filters: (@filters - kind_filters + [['uuid', 'is_a', type]]),
200                                     )
201           objects.each do |object|
202             @name_link_for[object.andand.uuid] = objects.links_for(object, 'name').first
203           end
204           @objects += objects
205         end
206       end
207       @objects = @objects.to_a.sort_by(&:created_at)
208       @objects.reverse! if nextpage_operator == '<='
209       @objects = @objects[0..@limit-1]
210       @next_page_filters = @filters.reject do |attr,op,val|
211         (attr == 'created_at' and op == nextpage_operator) or
212           (attr == 'uuid' and op == 'not in')
213       end
214
215       if @objects.any?
216         last_created_at = @objects.last.created_at
217
218         last_uuids = []
219         @objects.each do |obj|
220           last_uuids << obj.uuid if obj.created_at.eql?(last_created_at)
221         end
222
223         @next_page_filters += [['created_at',
224                                 nextpage_operator,
225                                 last_created_at]]
226         @next_page_filters += [['uuid', 'not in', last_uuids]]
227         @next_page_href = url_for(partial: :contents_rows,
228                                   limit: @limit,
229                                   filters: @next_page_filters.to_json)
230       else
231         @next_page_href = nil
232       end
233     else
234       @objects = @object.contents(order: @order,
235                                   limit: @limit,
236                                   include_linked: true,
237                                   filters: @filters,
238                                   offset: @offset)
239       @next_page_href = next_page_href(partial: :contents_rows,
240                                        filters: @filters.to_json,
241                                        order: @order.to_json)
242     end
243
244     preload_links_for_objects(@objects.to_a)
245   end
246
247   def show
248     if !@object
249       return render_not_found("object not found")
250     end
251
252     if params[:partial]
253       load_contents_objects
254       respond_to do |f|
255         f.json {
256           render json: {
257             content: render_to_string(partial: 'show_contents_rows.html',
258                                       formats: [:html]),
259             next_page_href: @next_page_href
260           }
261         }
262       end
263     else
264       @objects = []
265       super
266     end
267   end
268
269   def create
270     @new_resource_attrs = (params['project'] || {}).merge(group_class: 'project')
271     @new_resource_attrs[:name] ||= 'New project'
272     super
273   end
274
275   def update
276     @updates = params['project']
277     super
278   end
279
280   helper_method :get_objects_and_names
281   def get_objects_and_names(objects=nil)
282     objects = @objects if objects.nil?
283     objects_and_names = []
284     objects.each do |object|
285       if objects.respond_to? :links_for and
286           !(name_links = objects.links_for(object, 'name')).empty?
287         name_links.each do |name_link|
288           objects_and_names << [object, name_link]
289         end
290       elsif @name_link_for.andand[object.uuid]
291         objects_and_names << [object, @name_link_for[object.uuid]]
292       elsif object.respond_to? :name
293         objects_and_names << [object, object]
294       else
295         objects_and_names << [object,
296                                Link.new(owner_uuid: @object.uuid,
297                                         tail_uuid: @object.uuid,
298                                         head_uuid: object.uuid,
299                                         link_class: "name",
300                                         name: "")]
301
302       end
303     end
304     objects_and_names
305   end
306 end