wrangler 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,142 @@
1
+ module Wrangler
2
+ # handles notifying (sending emails) for the wrangler gem. configuration for
3
+ # the class is set in an initializer via the configure() method, e.g.
4
+ #
5
+ # Wrangler::ExceptionNotifier.configure do |config|
6
+ # config[:key] = value
7
+ # end
8
+ #
9
+ # see README or source code for possible values and default settings
10
+ #-----------------------------------------------------------------------------
11
+ class ExceptionNotifier < ActionMailer::Base
12
+ # the default configuration
13
+ @@config ||= {
14
+ # who the emails will be coming from. if nil or missing or empty string,
15
+ # effectively disables email notification
16
+ :from_address => '',
17
+ # array of addresses that the emails will be sent to. if nil or missing
18
+ # or empty array, effectively disables email notification.
19
+ :recipient_addresses => [],
20
+ # what will show up at the beginning of the subject line for each email
21
+ # sent note: will be preceded by "[<app_name (if any)>...", where app_name
22
+ # is the :app_name config value from ExceptionHandler (or explicit
23
+ # proc_name given to notify_on_error() method)
24
+ :subject_prefix => "#{(defined?(Rails) ? Rails.env : RAILS_ENV).capitalize} ERROR",
25
+ # can use this to define app-specific mail views using the same data
26
+ # made available in exception_notification()
27
+ :mailer_template_root => File.join(WRANGLER_ROOT, 'views')
28
+ }
29
+
30
+ cattr_accessor :config
31
+
32
+ def self.configure(&block)
33
+ yield @@config
34
+ end
35
+
36
+ self.template_root = config[:mailer_template_root]
37
+
38
+ # form and send the notification email (note: this gets called indirectly
39
+ # when you call ExceptionNotifier.deliver_exception_notification())
40
+ #
41
+ # arguments:
42
+ # - exception: the exception that was raised
43
+ # - proc_name: the name of the process in which the exception arised
44
+ # - backtrace: the stack trace from the exception (passing in excplicitly
45
+ # because serializing the exception does not preserve the
46
+ # backtrace (in case delayed_job is used to async the email)
47
+ # - status_code: the (string) http status code that the exception has been
48
+ # mapped to. Optional, but no default is provided, so
49
+ # no status code info will be contained in the email.
50
+ # - request_data: hash with relevant data from the request object.
51
+ # Optional, but if not present, then assumed the exception
52
+ # was not due to an http request and thus no request
53
+ # data will be contained in the email.
54
+ # - request: the original request that resulted in the exception. may
55
+ # be nil and MUST be nil if calling this method with
56
+ # delayed_job. Optional.
57
+ #---------------------------------------------------------------------------
58
+ def exception_notification(exception, proc_name, backtrace,
59
+ status_code = nil,
60
+ request_data = nil,
61
+ request = nil)
62
+
63
+ # don't try to send email if there are no from or recipient addresses
64
+ if config[:from_address].nil? ||
65
+ config[:from_address].empty? ||
66
+ config[:recipient_addresses].nil? ||
67
+ config[:recipient_addresses].empty?
68
+
69
+ return nil
70
+ end
71
+
72
+ ensure_session_loaded(request)
73
+
74
+ # NOTE: be very careful pulling data out of request in the view...it is
75
+ # NOT cleaned, and may contain private data (e.g. passwords), so
76
+ # scrutinize any use of @request in the views!
77
+
78
+ body_hash =
79
+ { :exception => exception,
80
+ :backtrace => backtrace,
81
+ :status_code => status_code,
82
+ :request_data => request_data,
83
+ :request => request
84
+ }
85
+
86
+ body_hash.merge! extract_data_from_request_data(request_data)
87
+ from config[:from_address]
88
+ recipients config[:recipient_addresses]
89
+ subject "[#{proc_name + (proc_name ? ' ' : '')}" +
90
+ "#{config[:subject_prefix]}] " +
91
+ "#{exception.class.name}: " +
92
+ "#{exception.message.inspect}"
93
+ body body_hash
94
+ sent_on Time.now
95
+ content_type 'text/plain'
96
+ end
97
+
98
+ # helper to force loading of session data in case of lazy loading (Rails
99
+ # 2.3). if the argument isn't a request object, then don't bother, cause
100
+ # it won't have session.
101
+ #---------------------------------------------------------------------------
102
+ def ensure_session_loaded(request)
103
+ request.session.inspect if !request.nil? && request.respond_to?(:session)
104
+ true
105
+ end
106
+
107
+ # extract relevant (and serializable) data from a request object
108
+ #---------------------------------------------------------------------------
109
+ def extract_data_from_request_data(request_data)
110
+ if request_data
111
+ { :host => host_from_request_data(request_data),
112
+ :protocol => protocol_from_request_data(request_data),
113
+ :uri => uri_from_request_data(request_data)
114
+ }
115
+ else
116
+ {}
117
+ end
118
+ end
119
+
120
+ # extract the host from request object
121
+ #---------------------------------------------------------------------------
122
+ def host_from_request_data(request_data)
123
+ request_data['HTTP_X_REAL_IP'] ||
124
+ request_data['HTTP_X_FORWARDED_HOST'] ||
125
+ request_data['HTTP_HOST']
126
+ end
127
+
128
+ # extract protocol from request object
129
+ #---------------------------------------------------------------------------
130
+ def protocol_from_request_data(request_data)
131
+ request_data['SERVER_PROTOCOL']
132
+ end
133
+
134
+ # extract URI from request object
135
+ #---------------------------------------------------------------------------
136
+ def uri_from_request_data(request_data)
137
+ request_data['REQUEST_URI']
138
+ end
139
+
140
+ end
141
+
142
+ end
@@ -0,0 +1,63 @@
1
+ # a bunch of handy exceptions. using the Http ones will require rails
2
+ module Wrangler
3
+
4
+ # base class for all the http-related exception classes
5
+ #-----------------------------------------------------------------------------
6
+ class HttpStatusError < Exception
7
+ def initialize(status_code, addl_msg = nil)
8
+ @status_code = status_code
9
+ @addl_msg = addl_msg
10
+
11
+ full_message = self.class.status_to_msg(status_code)
12
+ full_message += addl_msg unless addl_msg.nil?
13
+
14
+ super(full_message)
15
+ end
16
+
17
+ attr_reader :status_code, :addl_msg
18
+
19
+ # accepts either a Fixnum status code or a symbol representation of a
20
+ # status code (e.g. 404 or :not_found)
21
+ # yes, this is total duplication of code in
22
+ # action_controller/status_codes.rb, but it's been made a private instance
23
+ # method in the module.
24
+ #---------------------------------------------------------------------------
25
+ def self.status_to_msg(status)
26
+ case status
27
+ when Fixnum then
28
+ "#{status} #{ActionController::StatusCodes::STATUS_CODES[status]}".strip
29
+ when Symbol then
30
+ status_to_msg(ActionController::StatusCodes::SYMBOL_TO_STATUS_CODE[status] ||
31
+ "500 Unknown Status #{status.inspect}")
32
+ else
33
+ status.to_s
34
+ end
35
+ end
36
+ end
37
+
38
+
39
+ class HttpUnauthorized < HttpStatusError
40
+ def initialize(msg = nil); super(401, msg); end
41
+ end
42
+
43
+ class HttpNotFound < HttpStatusError
44
+ def initialize(msg = nil); super(404, msg); end
45
+ end
46
+
47
+ class HttpNotAcceptable < HttpStatusError
48
+ def initialize(msg = nil); super(406, msg); end
49
+ end
50
+
51
+ class HttpInternalServerError < HttpStatusError
52
+ def initialize(msg = nil); super(500, msg); end
53
+ end
54
+
55
+ class HttpNotImplemented < HttpStatusError
56
+ def initialize(msg = nil); super(501, msg); end
57
+ end
58
+
59
+ class HttpServiceUnavailable < HttpStatusError
60
+ def initialize(msg = nil); super(503, msg); end
61
+ end
62
+
63
+ end
@@ -0,0 +1,95 @@
1
+ module Wrangler
2
+ WRANGLER_ROOT = "#{File.dirname(__FILE__)}/../.."
3
+
4
+ # make all of these instance methods act as module functions as well
5
+ # (any instance method below this gets added as a module function as well)
6
+ module_function
7
+
8
+ # utility to determine if a class has another class as its ancestor.
9
+ #
10
+ # returns the ancestor class (if any) that is found to be the ancestor
11
+ # of klass (will return the nearest ancestor in other_klasses).
12
+ # returns nil or false otherwise.
13
+ #
14
+ # arguments:
15
+ # - klass: the class we're determining whether it has one of the other
16
+ # classes as an ancestor
17
+ # - other_klasses: a Class, an Array (or any other container that responds
18
+ # to include?() ) of Classes
19
+ #-----------------------------------------------------------------------------
20
+ def class_has_ancestor?(klass, other_klasses)
21
+ return nil if !klass.is_a?(Class)
22
+
23
+ other_klasses = [other_klasses] if other_klasses.is_a?(Class)
24
+
25
+ current_klass = klass
26
+ while current_klass
27
+ return current_klass if other_klasses.include?(current_klass)
28
+ current_klass = current_klass.superclass
29
+ end
30
+
31
+ return false
32
+ end
33
+
34
+
35
+ # given an array of search directory strings (or a single directory string),
36
+ # searches for files matching pattern.
37
+ #
38
+ # pattern expressed in cmd line wildcards...like "*.rb" or "foo.?"...
39
+ # and may contain subdirectories.
40
+ #---------------------------------------------------------------------------
41
+ def find_file_matching_pattern(search_dirs, pattern)
42
+ search_dirs = [search_dirs] unless search_dirs.is_a?(Array)
43
+
44
+ search_dirs.each do |d|
45
+ matches = Dir.glob(File.join(d, pattern))
46
+ return matches.first if matches.size > 0
47
+ end
48
+ return nil
49
+ end
50
+
51
+ # log the exception using logger if available. if object does not have a
52
+ # logger, will just puts()
53
+ #-----------------------------------------------------------------------------
54
+ def log_exception(exception, request_data = nil, status_code = nil)
55
+ msgs = []
56
+
57
+ msgs << "An exception was caught (#{exception.class.name}):"
58
+ msgs << exception.message
59
+ unless request_data.blank?
60
+ msgs << "Request params were:"
61
+ msgs << request_data.inspect
62
+ end
63
+ unless status_code.blank?
64
+ msgs << "Handling with status code: #{status_code}"
65
+ end
66
+ unless exception.backtrace.blank?
67
+ msgs << exception.backtrace.join("\n ")
68
+ end
69
+
70
+ log_error msgs
71
+ end
72
+
73
+ # handles logging error messages, using logger if available and puts otherwise
74
+ #-----------------------------------------------------------------------------
75
+ def log_error(msgs)
76
+ unless msgs.is_a?(Array)
77
+ msgs = [msgs]
78
+ end
79
+
80
+ msgs.each do |m|
81
+ if respond_to?(:logger)
82
+ logger.error m
83
+ else
84
+ puts m
85
+ end
86
+ end
87
+ end
88
+
89
+ # shorthand access to the exception handling config
90
+ #-----------------------------------------------------------------------------
91
+ def config
92
+ Wrangler::ExceptionHandler.config
93
+ end
94
+
95
+ end
data/lib/wrangler.rb ADDED
@@ -0,0 +1,284 @@
1
+ require 'wrangler/wrangler_helper.rb'
2
+ require 'wrangler/exception_handler.rb'
3
+ require 'wrangler/exception_notifier.rb'
4
+
5
+ module Wrangler
6
+
7
+ def self.included(base)
8
+ # only add in the controller-specific methods if the including class is one
9
+ if class_has_ancestor?(base, ActionController::Base)
10
+ base.send(:include, ControllerMethods)
11
+
12
+ # conditionally including these methods (each wrapped in a separate
13
+ # module) based on the configuration of whether to handle exceptions in
14
+ # the given environment or not. this allows the default implementation
15
+ # of the two rescue methods to run when Wrangler-based exception handling
16
+ # is disabled.
17
+
18
+ if Wrangler::ExceptionHandler.config[:handle_public_errors]
19
+ Rails.logger.info "Configuring #{base.name} with Wrangler's rescue_action_in_public"
20
+ base.send(:include, PublicControllerMethods)
21
+ else
22
+ Rails.logger.info "NOT Configuring #{base.name} with Wrangler's rescue_action_in_public"
23
+ end
24
+
25
+ if Wrangler::ExceptionHandler.config[:handle_local_errors]
26
+ Rails.logger.info "Configuring #{base.name} with Wrangler's rescue_action_locally"
27
+ base.send(:include, LocalControllerMethods)
28
+ else
29
+ Rails.logger.info "NOT configuring #{base.name} with Wrangler's rescue_action_locally"
30
+ end
31
+ end
32
+ end
33
+
34
+
35
+ # methods to only be included into a controller if Wrangler is configured to
36
+ # handle exceptions for public reqeusts. (Conditionally included into
37
+ # controllers in the Wrangler::included() method).
38
+ #-----------------------------------------------------------------------------
39
+ module PublicControllerMethods
40
+ # override default behavior and let Wrangler handle the exception for
41
+ # public (non-local) requests.
42
+ #---------------------------------------------------------------------------
43
+ def rescue_action_in_public(exception)
44
+ handle_exception(exception, :request => request,
45
+ :render_errors => true)
46
+ end
47
+ end
48
+
49
+
50
+ # methods to only be included into a controller if Wrangler is configured to
51
+ # handle exceptions for local reqeusts. (Conditionally included into
52
+ # controllers in the Wrangler::included() method).
53
+ #-----------------------------------------------------------------------------
54
+ module LocalControllerMethods
55
+ # override default behavior and let Wrangler handle the exception for
56
+ # local requests.
57
+ #---------------------------------------------------------------------------
58
+ def rescue_action_locally(exception)
59
+ handle_exception(exception, :request => request,
60
+ :render_errors => true)
61
+ end
62
+ end
63
+
64
+
65
+ # module of instance methods to be added to the class including Wrangler
66
+ # only if the including class is a rails controller class
67
+ #-----------------------------------------------------------------------------
68
+ module ControllerMethods
69
+
70
+ # called by rails if the exception has already been handled (e.g. by
71
+ # calling the rescue_from method in a controller and rendering a response)
72
+ #---------------------------------------------------------------------------
73
+ def rescue_with_handler(exception)
74
+ to_return = super
75
+ if to_return &&
76
+ (
77
+ (local_request? && Wrangler::ExceptionHandler.config[:handle_local_errors]) ||
78
+ (!local_request? && Wrangler::ExceptionHandler.config[:handle_public_errors])
79
+ )
80
+
81
+ handle_exception(exception, :request => request,
82
+ :render_errors => false)
83
+ end
84
+ to_return
85
+ end
86
+
87
+
88
+ # extract a hash of relevant (and serializable) parameters from a request
89
+ #---------------------------------------------------------------------------
90
+ def request_data_from_request(request)
91
+ return nil if request.nil?
92
+
93
+ request_data = {}
94
+ request.env.each_pair do |k,v|
95
+ next if skip_request_env?(k)
96
+
97
+ if self.respond_to?(:filter_parameters)
98
+ request_data.merge! self.send(:filter_parameters, k => v)
99
+ else
100
+ request_data.merge! k => v
101
+ end
102
+ end
103
+
104
+ params = {}
105
+ if self.respond_to?(:filter_parameters)
106
+ params.merge!(
107
+ filter_parameters(request.env['action_controller.request.query_parameters'])
108
+ )
109
+ params.merge!(
110
+ filter_parameters(request.env['action_controller.request.request_parameters'])
111
+ )
112
+ else
113
+ params.merge! request.env['action_controller.request.query_parameters']
114
+ params.merge! request.env['action_controller.request.request_parameters']
115
+ end
116
+
117
+ request_data.merge! :params => params unless params.blank?
118
+
119
+ return request_data
120
+ end
121
+
122
+
123
+ # determine if the request env param should be ommitted from the request
124
+ # data object, as specified in config (either for aesthetic reasons or
125
+ # because the param won't serialize well).
126
+ #---------------------------------------------------------------------------
127
+ def skip_request_env?(request_param)
128
+ skip_env = false
129
+ Wrangler::ExceptionHandler.config[:request_env_to_skip].each do |pattern|
130
+ if (pattern.is_a?(String) && pattern == request_param) ||
131
+ (pattern.is_a?(Regexp) && pattern =~ request_param)
132
+ skip_env = true
133
+ break
134
+ end
135
+ end
136
+
137
+ return skip_env
138
+ end
139
+
140
+
141
+ # select the proper file to render and do so. if the usual places don't
142
+ # turn up an appropriate template (see README), then fall back on
143
+ # an app-specific default error page or the ultimate back up gem default
144
+ # page.
145
+ #---------------------------------------------------------------------------
146
+ def render_error_template(exception, status_code)
147
+ file_path = get_view_path_for_exception(exception, status_code)
148
+
149
+ # if that didn't work, fall back on configured app-specific default
150
+ if file_path.blank? || !File.exists?(file_path)
151
+ file_path = config[:default_error_template]
152
+
153
+ log_error(["Could not find an error template in the usual places " +
154
+ "for exception #{exception.class}, status code " +
155
+ "#{status_code}.",
156
+ "Trying to default to app-specific default: '#{file_path}'"])
157
+ end
158
+
159
+ # as a last resort, just render the gem's 500 error
160
+ if file_path.blank? || !File.exists?(file_path)
161
+ file_path = config[:absolute_last_resort_default_error_template]
162
+
163
+ log_error("Still no template found. Using gem default of " +
164
+ file_path)
165
+ end
166
+
167
+ log_error("Will render error template: '#{file_path}'")
168
+
169
+ render :file => file_path,
170
+ :status => status_code
171
+ end
172
+
173
+
174
+ # select the appropriate view path for the exception/status code. see
175
+ # README or the code for the different attempts that are made to find
176
+ # a template.
177
+ #---------------------------------------------------------------------------
178
+ def get_view_path_for_exception(exception, status_code)
179
+
180
+ # maintenance note: this method has lots of RETURN statements, so be
181
+ # a little careful, but basically, it returns as soon as it finds a
182
+ # file match, so there shouldn't be any processing to perform after
183
+ # a match is found. any such logic does not belong in this method
184
+
185
+ if exception.is_a?(Class)
186
+ exception_class = exception
187
+ else
188
+ exception_class = exception.class
189
+ end
190
+
191
+ # Note: this converts "::" to "/", so views need to be nested under
192
+ # exceptions' modules if appropriate
193
+ exception_filename_root = exception_class.name.underscore
194
+
195
+ template_mappings = nil
196
+ case request.format
197
+ when /html/
198
+ response_format = 'html'
199
+ template_mappings = config[:error_class_html_templates]
200
+ when /js/
201
+ response_format = 'js'
202
+ template_mappings = config[:error_class_js_templates]
203
+ when /xml/
204
+ 'xml'
205
+ end
206
+ format_extension_pattern = ".#{response_format || ''}*"
207
+
208
+ if template_mappings
209
+
210
+ # search for direct mapping from exception name to error template
211
+
212
+ if template_mappings[exception_class]
213
+ error_file = template_mappings[exception_class]
214
+
215
+ return error_file if File.exists?(error_file)
216
+
217
+ log_error("Found mapping from exception class " +
218
+ "#{exception_class.name} to error file '#{error_file}', " +
219
+ "but error file was not found")
220
+ end
221
+
222
+ # search for mapping from an ancestor class to error template
223
+
224
+ ancestor_class =
225
+ Wrangler::class_has_ancestor?(exception_class.superclass,
226
+ template_mappings)
227
+
228
+ if ancestor_class
229
+ error_file = template_mappings[ancestor_class]
230
+
231
+ return error_file if File.exists?(error_file)
232
+
233
+ log_error("Found mapping from ancestor exception class " +
234
+ "#{ancestor_class.name} to error file '#{error_file}', " +
235
+ "but error file was not found")
236
+ end
237
+
238
+ end # end if template_mappings
239
+
240
+ # search for a file named after the exception in one of the search dirs
241
+
242
+ search_paths = [ config[:error_template_dir],
243
+ File.join(RAILS_ROOT, 'public'),
244
+ File.join(WRANGLER_ROOT, 'rails', 'app', 'views', 'wrangler')
245
+ ]
246
+
247
+ # find files in specified directory like 'exception_class_name.format', e.g.
248
+ # standard_error.html or standard_error.js.erb
249
+ exception_pattern = "#{exception_filename_root}#{format_extension_pattern}"
250
+ file_path = find_file_matching_pattern(search_paths, exception_pattern)
251
+
252
+ return file_path if file_path
253
+
254
+ # search for a file named after the error status code in search dirs
255
+
256
+ status_code_pattern = "#{status_code}#{format_extension_pattern}"
257
+ file_path = find_file_matching_pattern(search_paths, status_code_pattern)
258
+
259
+ return file_path if file_path
260
+
261
+ # search for a file named after ancestors of the exception in search dirs
262
+
263
+ # look through exception's entire ancenstry to see if there's a matching
264
+ # template in the search directories
265
+ curr_ancestor = exception_class.superclass
266
+ while curr_ancestor
267
+ # find files in specified directory like 'exception_class_name.format', e.g.
268
+ # standard_error.html or standard_error.js.erb
269
+ exception_pattern =
270
+ "#{curr_ancestor.name.underscore}#{format_extension_pattern}"
271
+ file_path = find_file_matching_pattern(search_paths, exception_pattern)
272
+
273
+ return file_path if file_path
274
+
275
+ curr_ancestor = curr_ancestor.superclass
276
+ end
277
+
278
+ # didn't find anything
279
+ return nil
280
+ end
281
+
282
+ end # end ControllerMethods module
283
+
284
+ end # end Wrangler module
@@ -0,0 +1,6 @@
1
+ <!-- This file lives in gems/wrangler/rails/app/views/wrangler. See README for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>Unable to Process Requested Data</h1>
4
+
5
+ <p>Reloading the page will probably not help.</p>
6
+ </div>
@@ -0,0 +1,6 @@
1
+ <!-- This file lives in gems/wrangler/rails/app/views/wrangler. See README for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>Not Authorized</h1>
4
+
5
+ <p>You are not authorized to access this resource. You may be either not logged in at all, or not logged in as the appropriate user.</p>
6
+ </div>
@@ -0,0 +1,6 @@
1
+ <!-- This file lives in gems/wrangler/rails/app/views/wrangler. See README for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>Access Restricted</h1>
4
+
5
+ <p>If you need access to this resource please contact support or an administrator.</p>
6
+ </div>
@@ -0,0 +1,6 @@
1
+ <!-- This file lives in gems/wrangler/rails/app/views/wrangler. See README for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>The page you were looking for doesn't exist.</h1>
4
+
5
+ <p>You may have mistyped the address or the page may have moved.</p>
6
+ </div>
@@ -0,0 +1,6 @@
1
+ <!-- This file lives in gems/wrangler/rails/app/views/wrangler. See README for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>Invalid Method.</h1>
4
+
5
+ <p>This resource only accepts certain HTTP methods.</p>
6
+ </div>
@@ -0,0 +1,7 @@
1
+ <!-- This file lives in gems/wrangler/rails/app/views/wrangler. See README for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>Requested resource is no longer available</h1>
4
+
5
+ <p>Please remove bookmarks to this resource.</p>
6
+ <p>If you arrived here via a link from somewhere else please inform them of the broken link.</p>
7
+ </div>
@@ -0,0 +1,6 @@
1
+ <!-- This file lives in gems/wrangler/rails/app/views/wrangler. See README for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>Unprocessable Request</h1>
4
+
5
+ <p>The request was well-formed but was unable to be followed due to semantic errors.</p>
6
+ </div>
@@ -0,0 +1,7 @@
1
+ <!-- This file lives in gems/wrangler/rails/app/views/wrangler. See README for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>The change you wanted was rejected because the resource is locked</h1>
4
+
5
+ <p>It might become available again soon, if you try again!</p>
6
+ <p>Maybe you tried to change something you didn't have access to.</p>
7
+ </div>
@@ -0,0 +1,6 @@
1
+ <!-- This file lives in gems/wrangler/rails/app/views/wrangler. See README for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>We're sorry, but something went wrong.</h1>
4
+
5
+ <p>We've been notified about this issue and we'll take a look at it shortly.</p>
6
+ </div>
@@ -0,0 +1,8 @@
1
+ <!-- This file lives in gems/wrangler/rails/app/views/wrangler. See README for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>Not Implemented</h1>
4
+
5
+ <p>Please remove bookmarks to this resource.</p>
6
+ <p>You have tried to access a feature that has not been implemented.</p>
7
+ <p>If you arrived here via a link from somewhere else please inform them of the broken link.</p>
8
+ </div>
@@ -0,0 +1,6 @@
1
+ <!-- This file lives in gems/wrangler/rails/app/views/wrangler. See README for instructions on customizing. -->
2
+ <div class="dialog">
3
+ <h1>The server is temporarily unavailable.</h1>
4
+
5
+ <p>The resource you requested is temporarilly unavailable due to server overload, maintenance, or other downtime. Generally, this is a temporary state.</p>
6
+ </div>