Refactor the GPX upload to try and avoid the import daemon loading traces
[rails.git] / app / controllers / trace_controller.rb
1 class TraceController < ApplicationController
2   layout 'site'
3
4   before_filter :authorize_web  
5   before_filter :require_user, :only => [:mine, :edit, :delete, :make_public]
6   before_filter :authorize, :only => [:api_details, :api_data, :api_create]
7   before_filter :check_database_availability, :except => [:api_details, :api_data, :api_create]
8   before_filter :check_read_availability, :only => [:api_details, :api_data, :api_create]
9  
10   # Counts and selects pages of GPX traces for various criteria (by user, tags, public etc.).
11   #  target_user - if set, specifies the user to fetch traces for.  if not set will fetch all traces
12   def list(target_user = nil, action = "list")
13     # from display name, pick up user id if one user's traces only
14     display_name = params[:display_name]
15     if target_user.nil? and !display_name.blank?
16       target_user = User.find(:first, :conditions => [ "visible = 1 and display_name = ?", display_name])
17     end
18
19     # set title
20     if target_user.nil?
21       @title = "Public GPS traces"
22     elsif @user and @user == target_user
23       @title = "Your GPS traces"
24     else
25       @title = "Public GPS traces from #{target_user.display_name}"
26     end
27
28     @title += " tagged with #{params[:tag]}" if params[:tag]
29
30     # four main cases:
31     # 1 - all traces, logged in = all public traces + all user's (i.e + all mine)
32     # 2 - all traces, not logged in = all public traces
33     # 3 - user's traces, logged in as same user = all user's traces 
34     # 4 - user's traces, not logged in as that user = all user's public traces
35     if target_user.nil? # all traces
36       if @user
37         conditions = ["(gpx_files.public = 1 OR gpx_files.user_id = ?)", @user.id] #1
38       else
39         conditions  = ["gpx_files.public = 1"] #2
40       end
41     else
42       if @user and @user == target_user
43         conditions = ["gpx_files.user_id = ?", @user.id] #3 (check vs user id, so no join + can't pick up non-public traces by changing name)
44       else
45         conditions = ["gpx_files.public = 1 AND gpx_files.user_id = ?", target_user.id] #4
46       end
47     end
48     
49     if params[:tag]
50       @tag = params[:tag]
51
52       files = Tracetag.find_all_by_tag(params[:tag]).collect { |tt| tt.gpx_id }
53       conditions[0] += " AND gpx_files.id IN (#{files.join(',')})"
54     end
55     
56     conditions[0] += " AND gpx_files.visible = 1"
57
58     @trace_pages, @traces = paginate(:traces,
59                                      :include => [:user, :tags],
60                                      :conditions => conditions,
61                                      :order => "gpx_files.timestamp DESC",
62                                      :per_page => 20)
63
64     # put together SET of tags across traces, for related links
65     tagset = Hash.new
66     if @traces
67       @traces.each do |trace|
68         trace.tags.reload if params[:tag] # if searched by tag, ActiveRecord won't bring back other tags, so do explicitly here
69         trace.tags.each do |tag|
70           tagset[tag.tag] = tag.tag
71         end
72       end
73     end
74     
75     # final helper vars for view
76     @action = action
77     @display_name = target_user.display_name if target_user
78     @all_tags = tagset.values
79   end
80
81   def mine
82     list(@user, "mine")
83   end
84
85   def view
86     @trace = Trace.find(params[:id])
87
88     if @trace and @trace.visible? and
89        (@trace.public? or @trace.user == @user)
90       @title = "Viewing trace #{@trace.name}"
91     else
92       flash[:notice] = "Trace not found!"
93       redirect_to :controller => 'trace', :action => 'list'
94     end
95   rescue ActiveRecord::RecordNotFound
96     flash[:notice] = "Trace not found!"
97     redirect_to :controller => 'trace', :action => 'list'
98   end
99
100   def create
101     logger.info(params[:trace][:gpx_file].class.name)
102     if params[:trace][:gpx_file].respond_to?(:read)
103       do_create(params[:trace][:gpx_file], params[:trace][:tagstring],
104                 params[:trace][:description], params[:trace][:public])
105
106       if @trace.id
107         logger.info("id is #{@trace.id}")
108         flash[:notice] = "Your GPX file has been uploaded and is awaiting insertion in to the database. This will usually happen within half an hour, and an email will be sent to you on completion."
109
110         redirect_to :action => 'mine'
111       end
112     else
113       @trace = Trace.new({:name => "Dummy",
114                           :tagstring => params[:trace][:tagstring],
115                           :description => params[:trace][:description],
116                           :public => params[:trace][:public],
117                           :inserted => false, :user => @user,
118                           :timestamp => Time.now})
119       @trace.valid?
120       @trace.errors.add(:gpx_file, "can't be blank")
121     end
122   end
123
124   def data
125     trace = Trace.find(params[:id])
126
127     if trace.visible? and (trace.public? or (@user and @user == trace.user))
128       if request.format == Mime::XML
129         send_file(trace.xml_file, :filename => "#{trace.id}.xml", :type => Mime::XML.to_s, :disposition => 'attachment')
130       else
131         send_file(trace.trace_name, :filename => "#{trace.id}#{trace.extension_name}", :type => trace.mime_type, :disposition => 'attachment')
132       end
133     else
134       render :nothing => true, :status => :not_found
135     end
136   rescue ActiveRecord::RecordNotFound
137     render :nothing => true, :status => :not_found
138   end
139
140   def edit
141     @trace = Trace.find(params[:id])
142
143     if @user and @trace.user == @user
144       if params[:trace]
145         @trace.description = params[:trace][:description]
146         @trace.tagstring = params[:trace][:tagstring]
147         if @trace.save
148           redirect_to :action => 'view'
149         end        
150       end
151     else
152       render :nothing => true, :status => :forbidden
153     end
154   rescue ActiveRecord::RecordNotFound
155     render :nothing => true, :status => :not_found
156   end
157
158   def delete
159     trace = Trace.find(params[:id])
160
161     if @user and trace.user == @user
162       if request.post? and trace.visible?
163         trace.visible = false
164         trace.save
165         flash[:notice] = 'Track scheduled for deletion'
166         redirect_to :controller => 'traces', :action => 'mine'
167       else
168         render :nothing => true, :status => :bad_request
169       end
170     else
171       render :nothing => true, :status => :forbidden
172     end
173   rescue ActiveRecord::RecordNotFound
174     render :nothing => true, :status => :not_found
175   end
176
177   def make_public
178     trace = Trace.find(params[:id])
179
180     if @user and trace.user == @user
181       if request.post? and !trace.public?
182         trace.public = true
183         trace.save
184         flash[:notice] = 'Track made public'
185         redirect_to :controller => 'trace', :action => 'view', :id => params[:id]
186       else
187         render :nothing => true, :status => :bad_request
188       end
189     else
190       render :nothing => true, :status => :forbidden
191     end
192   rescue ActiveRecord::RecordNotFound
193     render :nothing => true, :status => :not_found
194   end
195
196   def georss
197     conditions = ["gpx_files.public = 1"]
198
199     if params[:display_name]
200       conditions[0] += " AND users.display_name = ?"
201       conditions << params[:display_name]
202     end
203
204     if params[:tag]
205       conditions[0] += " AND EXISTS (SELECT * FROM gpx_file_tags AS gft WHERE gft.gpx_id = gpx_files.id AND gft.tag = ?)"
206       conditions << params[:tag]
207     end
208
209     traces = Trace.find(:all, :include => :user, :conditions => conditions, 
210                         :order => "timestamp DESC", :limit => 20)
211
212     rss = OSM::GeoRSS.new
213
214     traces.each do |trace|
215       rss.add(trace.latitude, trace.longitude, trace.name, trace.user.display_name, url_for({:controller => 'trace', :action => 'view', :id => trace.id, :display_name => trace.user.display_name}), "<img src='#{url_for({:controller => 'trace', :action => 'icon', :id => trace.id, :user_login => trace.user.display_name})}'> GPX file with #{trace.size} points from #{trace.user.display_name}", trace.timestamp)
216     end
217
218     render :text => rss.to_s, :content_type => "application/rss+xml"
219   end
220
221   def picture
222     trace = Trace.find(params[:id])
223
224     if trace.inserted?
225       if trace.public? or (@user and @user == trace.user)
226         send_file(trace.large_picture_name, :filename => "#{trace.id}.gif", :type => 'image/gif', :disposition => 'inline')
227       else
228         render :nothing => true, :status => :forbidden
229       end
230     else
231       render :nothing => true, :status => :not_found
232     end
233   rescue ActiveRecord::RecordNotFound
234     render :nothing => true, :status => :not_found
235   end
236
237   def icon
238     trace = Trace.find(params[:id])
239
240     if trace.inserted?
241       if trace.public? or (@user and @user == trace.user)
242         send_file(trace.icon_picture_name, :filename => "#{trace.id}_icon.gif", :type => 'image/gif', :disposition => 'inline')
243       else
244         render :nothing => true, :status => :forbidden
245       end
246     else
247       render :nothing => true, :status => :not_found
248     end
249   rescue ActiveRecord::RecordNotFound
250     render :nothing => true, :status => :not_found
251   end
252
253   def api_details
254     trace = Trace.find(params[:id])
255
256     if trace.public? or trace.user == @user
257       render :text => trace.to_xml.to_s, :content_type => "text/xml"
258     else
259       render :nothing => true, :status => :forbidden
260     end
261   rescue ActiveRecord::RecordNotFound
262     render :nothing => true, :status => :not_found
263   end
264
265   def api_data
266     trace = Trace.find(params[:id])
267
268     if trace.public? or trace.user == @user
269       send_file(trace.trace_name, :filename => "#{trace.id}#{trace.extension_name}", :type => trace.mime_type, :disposition => 'attachment')
270     else
271       render :nothing => true, :status => :forbidden
272     end
273   rescue ActiveRecord::RecordNotFound
274     render :nothing => true, :status => :not_found
275   end
276
277   def api_create
278     if request.post?
279       do_create(params[:file], params[:tags], params[:description], params[:public])
280
281       if @trace.id
282         render :text => @trace.id.to_s, :content_type => "text/plain"
283       elsif @trace.valid?
284         render :nothing => true, :status => :internal_server_error
285       else
286         render :nothing => true, :status => :bad_request
287       end
288     else
289       render :nothing => true, :status => :method_not_allowed
290     end
291   end
292
293 private
294
295   def do_create(file, tags, description, public)
296     # Sanitise the user's filename
297     name = file.original_filename.gsub(/[^a-zA-Z0-9.]/, '_')
298
299     # Get a temporary filename...
300     filename = "/tmp/#{rand}"
301
302     # ...and save the uploaded file to that location
303     File.open(filename, "w") { |f| f.write(file.read) }
304
305     # Create the trace object, falsely marked as already
306     # inserted to stop the import daemon trying to load it
307     @trace = Trace.new({
308       :name => name,
309       :tagstring => tags,
310       :description => description,
311       :public => public,
312       :inserted => true,
313       :user => @user,
314       :timestamp => Time.now
315     })
316
317     # Save the trace object
318     if @trace.save
319       # Rename the temporary file to the final name
320       FileUtils.mv(filename, @trace.trace_name)
321
322       # Clear the inserted flag to make the import daemon load the trace
323       @trace.inserted = false
324       @trace.save!
325     else
326       # Remove the file as we have failed to update the database
327       FileUtils.rm_f(filename)
328     end
329   end
330
331 end