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