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