Merge branch 'master' into 3637-copy-selections
[arvados.git] / apps / workbench / app / controllers / projects_controller.rb
1 class ProjectsController < ApplicationController
2   def model_class
3     Group
4   end
5
6   def find_object_by_uuid
7     if current_user and params[:uuid] == current_user.uuid
8       @object = current_user.dup
9       @object.uuid = current_user.uuid
10       class << @object
11         def name
12           'Home'
13         end
14         def description
15           ''
16         end
17         def attribute_editable? attr, *args
18           case attr
19           when 'description', 'name'
20             false
21           else
22             super
23           end
24         end
25       end
26     else
27       super
28     end
29   end
30
31   def index_pane_list
32     %w(Projects)
33   end
34
35   def show_pane_list
36     if @user_is_manager
37       %w(Data_collections Jobs_and_pipelines Pipeline_templates Subprojects Other_objects Sharing Advanced)
38     else
39       %w(Data_collections Jobs_and_pipelines Pipeline_templates Subprojects Other_objects Advanced)
40     end
41   end
42
43   def remove_item
44     params[:item_uuids] = [params[:item_uuid]]
45     remove_items
46     render template: 'projects/remove_items'
47   end
48
49   def remove_items
50     @removed_uuids = []
51     links = []
52     params[:item_uuids].collect { |uuid| ArvadosBase.find uuid }.each do |item|
53       if (item.class == Link and
54           item.link_class == 'name' and
55           item.tail_uuid == @object.uuid)
56         # Given uuid is a name link, linking an object to this
57         # project. First follow the link to find the item we're removing,
58         # then delete the link.
59         links << item
60         item = ArvadosBase.find item.head_uuid
61       else
62         # Given uuid is an object. Delete all names.
63         links += Link.where(tail_uuid: @object.uuid,
64                             head_uuid: item.uuid,
65                             link_class: 'name')
66       end
67       links.each do |link|
68         @removed_uuids << link.uuid
69         link.destroy
70       end
71       if item.owner_uuid == @object.uuid
72         # Object is owned by this project. Remove it from the project by
73         # changing owner to the current user.
74         item.update_attributes owner_uuid: current_user.uuid
75         @removed_uuids << item.uuid
76       end
77     end
78   end
79
80   def copy_items
81     move_or_copy_items :copy
82   end
83
84   def move_items
85     move_or_copy_items :move
86   end
87
88   def move_or_copy_items action
89     target_uuid = params['target']
90     uuids_to_add = session[:selected_move_or_copy_items]
91
92     uuids_to_add.
93       collect { |x| ArvadosBase::resource_class_for_uuid(x) }.
94       uniq.
95       each do |resource_class|
96       resource_class.filter([['uuid','in',uuids_to_add]]).each do |src|
97         if resource_class == Collection and not Collection.attribute_info.include?(:name)
98           dst = Link.new(owner_uuid: target_uuid,
99                          tail_uuid: target_uuid,
100                          head_uuid: src.uuid,
101                          link_class: 'name',
102                          name: src.uuid)
103         else
104           case action
105           when :copy
106             dst = src.dup
107             if dst.respond_to? :'name='
108               if dst.name
109                 dst.name = "Copy of #{dst.name}"
110               else
111                 dst.name = "Copy of unnamed #{dst.class_for_display.downcase}"
112               end
113             end
114             if resource_class == Collection
115               dst.manifest_text = Collection.select([:manifest_text]).where(uuid: src.uuid).first.manifest_text
116             end
117           when :move
118             dst = src
119           else
120             raise ArgumentError.new "Unsupported action #{action}"
121           end
122           dst.owner_uuid = target_uuid
123           dst.tail_uuid = target_uuid if dst.class == Link
124         end
125         dst.save!
126       end
127     end
128     session[:selected_move_or_copy_items] = nil
129     redirect_to @object
130   end
131
132   def destroy
133     while (objects = Link.filter([['owner_uuid','=',@object.uuid],
134                                   ['tail_uuid','=',@object.uuid]])).any?
135       objects.each do |object|
136         object.destroy
137       end
138     end
139     while (objects = @object.contents(include_linked: false)).any?
140       objects.each do |object|
141         object.update_attributes! owner_uuid: current_user.uuid
142       end
143     end
144     if ArvadosBase::resource_class_for_uuid(@object.owner_uuid) == Group
145       params[:return_to] ||= group_path(@object.owner_uuid)
146     else
147       params[:return_to] ||= projects_path
148     end
149     super
150   end
151
152   def find_objects_for_index
153     @objects = all_projects
154     super
155   end
156
157   def load_contents_objects kinds=[]
158     kind_filters = @filters.select do |attr,op,val|
159       op == 'is_a' and val.is_a? Array and val.count > 1
160     end
161     if /^created_at\b/ =~ @order[0] and kind_filters.count == 1
162       # If filtering on multiple types and sorting by date: Get the
163       # first page of each type, sort the entire set, truncate to one
164       # page, and use the last item on this page as a filter for
165       # retrieving the next page. Ideally the API would do this for
166       # us, but it doesn't (yet).
167       nextpage_operator = /\bdesc$/i =~ @order[0] ? '<' : '>'
168       @objects = []
169       @name_link_for = {}
170       kind_filters.each do |attr,op,val|
171         (val.is_a?(Array) ? val : [val]).each do |type|
172           objects = @object.contents(order: @order,
173                                      limit: @limit,
174                                      include_linked: true,
175                                      filters: (@filters - kind_filters + [['uuid', 'is_a', type]]),
176                                      offset: @offset)
177           objects.each do |object|
178             @name_link_for[object.andand.uuid] = objects.links_for(object, 'name').first
179           end
180           @objects += objects
181         end
182       end
183       @objects = @objects.to_a.sort_by(&:created_at)
184       @objects.reverse! if nextpage_operator == '<'
185       @objects = @objects[0..@limit-1]
186       @next_page_filters = @filters.reject do |attr,op,val|
187         attr == 'created_at' and op == nextpage_operator
188       end
189       if @objects.any?
190         @next_page_filters += [['created_at',
191                                 nextpage_operator,
192                                 @objects.last.created_at]]
193         @next_page_href = url_for(partial: :contents_rows,
194                                   filters: @next_page_filters.to_json)
195       else
196         @next_page_href = nil
197       end
198     else
199       @objects = @object.contents(order: @order,
200                                   limit: @limit,
201                                   include_linked: true,
202                                   filters: @filters,
203                                   offset: @offset)
204       @next_page_href = next_page_href(partial: :contents_rows)
205     end
206
207     preload_links_for_objects(@objects.to_a)
208   end
209
210   def show
211     if !@object
212       return render_not_found("object not found")
213     end
214
215     @user_is_manager = false
216     @share_links = []
217     if @object.uuid != current_user.uuid
218       begin
219         @share_links = Link.permissions_for(@object)
220         @user_is_manager = true
221       rescue ArvadosApiClient::AccessForbiddenException,
222         ArvadosApiClient::NotFoundException
223       end
224     end
225
226     if params[:partial]
227       load_contents_objects
228       respond_to do |f|
229         f.json {
230           render json: {
231             content: render_to_string(partial: 'show_contents_rows.html',
232                                       formats: [:html]),
233             next_page_href: @next_page_href
234           }
235         }
236       end
237     else
238       @objects = []
239       super
240     end
241   end
242
243   def create
244     @new_resource_attrs = (params['project'] || {}).merge(group_class: 'project')
245     @new_resource_attrs[:name] ||= 'New project'
246     super
247   end
248
249   def update
250     @updates = params['project']
251     super
252   end
253
254   helper_method :get_objects_and_names
255   def get_objects_and_names(objects=nil)
256     objects = @objects if objects.nil?
257     objects_and_names = []
258     objects.each do |object|
259       if objects.respond_to? :links_for and
260           !(name_links = objects.links_for(object, 'name')).empty?
261         name_links.each do |name_link|
262           objects_and_names << [object, name_link]
263         end
264       elsif @name_link_for.andand[object.uuid]
265         objects_and_names << [object, @name_link_for[object.uuid]]
266       elsif object.respond_to? :name
267         objects_and_names << [object, object]
268       elsif not Collection.attribute_info.include?(:name)
269         objects_and_names << [object,
270                                Link.new(owner_uuid: @object.uuid,
271                                         tail_uuid: @object.uuid,
272                                         head_uuid: object.uuid,
273                                         link_class: "name",
274                                         name: "")]
275       end
276     end
277     objects_and_names
278   end
279
280   def share_with
281     if not params[:uuids].andand.any?
282       @errors = ["No user/group UUIDs specified to share with."]
283       return render_error(status: 422)
284     end
285     results = {"success" => [], "errors" => []}
286     params[:uuids].each do |shared_uuid|
287       begin
288         Link.create(tail_uuid: shared_uuid, link_class: "permission",
289                     name: "can_read", head_uuid: @object.uuid)
290       rescue ArvadosApiClient::ApiError => error
291         error_list = error.api_response.andand[:errors]
292         if error_list.andand.any?
293           results["errors"] += error_list.map { |e| "#{shared_uuid}: #{e}" }
294         else
295           error_code = error.api_status || "Bad status"
296           results["errors"] << "#{shared_uuid}: #{error_code} response"
297         end
298       else
299         results["success"] << shared_uuid
300       end
301     end
302     if results["errors"].empty?
303       results.delete("errors")
304       status = 200
305     else
306       status = 422
307     end
308     respond_to do |f|
309       f.json { render(json: results, status: status) }
310     end
311   end
312 end