1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
|
from collections import Iterable
from typing import Any
from flask import Blueprint, Response, jsonify, redirect, render_template, url_for
from flask.views import MethodView
from werkzeug.exceptions import default_exceptions
from pysite.constants import DEBUG_MODE, ErrorCodes
from pysite.mixins import OauthMixin
class BaseView(MethodView, OauthMixin):
"""
Base view class with functions and attributes that should be common to all view classes.
This class should be subclassed, and is not intended to be used directly.
"""
name = None # type: str
blueprint = None # type: str
def render(self, *template_names: str, **context: Any) -> str:
"""
Render some templates and get them back in a form that you can simply return from your view function.
Here's what's inserted:
* "current_page" - the "name" attribute from the view class
* "view" - the view class instance
* "logged_in" - a boolean, True if the user is logged in
* "static_file(filename)", a function used to get the URL for a given static file
* "csrf_token()", a function returning the CSRF token stored in the current session
For XSS protection, a CSRF token must be used. The "csrf_token()" function returns the correct token
to be used in the current rendering context - if your view methods are to be protected from XSS
exploits, the following steps must be taken:
1. Apply the "csrf" decorator to the view method
2. For forms, a hidden input must be declared in the template, with the name "csrf_token", and the value set to
the CSRF token.
3. For any AJAX work, the CSRF token should be stored in a variable, and sent as part of the request headers.
You can set the "X-CSRFToken" header to the CSRF token for this.
Any API call or form submission not protected by an API key must not be vulnerable to XSS, unless the API
call is intended to be a completely public feature. Public API methods must not be account-bound, and they
must never return information on a current user or perform any action. Only data retrieval is permissible.
:param template_names: Names of the templates to render
:param context: Extra data to pass into the template
:return: String representing the rendered templates
"""
context["current_page"] = self.name
context["view"] = self
context["logged_in"] = self.logged_in
context["static_file"] = self._static_file
context["debug"] = DEBUG_MODE
context["format_datetime"] = lambda dt: dt.strftime("%b %d %Y, %H:%M")
return render_template(template_names, **context)
def _static_file(self, filename):
return url_for("static", filename=filename)
class RouteView(BaseView):
"""
Standard route-based page view. For a standard page, this is what you want.
This class is intended to be subclassed - use it as a base class for your own views, and set the class-level
attributes as appropriate. For example:
>>> class MyView(RouteView):
... name = "my_view" # Flask internal name for this route
... path = "/my_view" # Actual URL path to reach this route
...
... def get(self): # Name your function after the relevant HTTP method
... return self.render("index.html")
For more complicated routing, see http://exploreflask.com/en/latest/views.html#built-in-converters
"""
path = None # type: str
@classmethod
def setup(cls: "RouteView", manager: "pysite.route_manager.RouteManager", blueprint: Blueprint):
"""
Set up the view by adding it to the blueprint passed in - this will also deal with multiple inheritance by
calling `super().setup()` as appropriate.
This is for a standard route view. Nothing special here.
:param manager: Instance of the current RouteManager
:param blueprint: Current Flask blueprint to register this route to
"""
if hasattr(super(), "setup"):
super().setup(manager, blueprint)
if not cls.path or not cls.name:
raise RuntimeError("Route views must have both `path` and `name` defined")
blueprint.add_url_rule(cls.path, view_func=cls.as_view(cls.name))
cls.name = f"{blueprint.name}.{cls.name}" # Add blueprint to page name
class APIView(RouteView):
"""
API route view, with extra methods to help you add routes to the JSON API with ease.
This class is intended to be subclassed - use it as a base class for your own views, and set the class-level
attributes as appropriate. For example:
>>> class MyView(APIView):
... name = "my_view" # Flask internal name for this route
... path = "/my_view" # Actual URL path to reach this route
...
... def get(self): # Name your function after the relevant HTTP method
... return self.error(ErrorCodes.unknown_route)
"""
def error(self, error_code: ErrorCodes, error_info: str = "") -> Response:
"""
Generate a JSON response for you to return from your handler, for a specific type of API error
:param error_code: The type of error to generate a response for - see `constants.ErrorCodes` for more
:param error_info: An optional message with more information about the error.
:return: A Flask Response object that you can return from your handler
"""
data = {
"error_code": error_code.value,
"error_message": error_info or "Unknown error"
}
http_code = 200
if error_code is ErrorCodes.unknown_route:
data["error_message"] = error_info or "Unknown API route"
http_code = 404
elif error_code is ErrorCodes.unauthorized:
data["error_message"] = error_info or "Unauthorized"
http_code = 401
elif error_code is ErrorCodes.invalid_api_key:
data["error_message"] = error_info or "Invalid API-key"
http_code = 401
elif error_code is ErrorCodes.bad_data_format:
data["error_message"] = error_info or "Input data in incorrect format"
http_code = 400
elif error_code is ErrorCodes.incorrect_parameters:
data["error_message"] = error_info or "Incorrect parameters provided"
http_code = 400
response = jsonify(data)
response.status_code = http_code
return response
class ErrorView(BaseView):
"""
Error view, shown for a specific HTTP status code, as defined in the class attributes.
This class is intended to be subclassed - use it as a base class for your own views, and set the class-level
attributes as appropriate. For example:
>>> class MyView(ErrorView):
... name = "my_view" # Flask internal name for this route
... path = "/my_view" # Actual URL path to reach this route
... error_code = 404 # Error code
...
... def get(self, error: HTTPException): # Name your function after the relevant HTTP method
... return "Replace me with a template, 404 not found", 404
If you'd like to catch multiple HTTP error codes, feel free to supply an iterable for `error_code`. For example...
>>> error_code = [401, 403] # Handle two specific errors
>>> error_code = range(500, 600) # Handle all 5xx errors
"""
error_code = None # type: Union[int, Iterable]
register_on_app = True
@classmethod
def setup(cls: "ErrorView", manager: "pysite.route_manager.RouteManager", blueprint: Blueprint):
"""
Set up the view by registering it as the error handler for the HTTP status codes specified in the class
attributes - this will also deal with multiple inheritance by calling `super().setup()` as appropriate.
:param manager: Instance of the current RouteManager
:param blueprint: Current Flask blueprint to register the error handler for
"""
if hasattr(super(), "setup"):
super().setup(manager, blueprint) # pragma: no cover
if not cls.name or not cls.error_code:
raise RuntimeError("Error views must have both `name` and `error_code` defined")
if isinstance(cls.error_code, int):
cls.error_code = [cls.error_code]
if isinstance(cls.error_code, Iterable):
for code in cls.error_code:
if isinstance(code, int) and code not in default_exceptions:
continue # Otherwise we'll possibly get an exception thrown during blueprint registration
if cls.register_on_app:
manager.app.errorhandler(code)(cls.as_view(cls.name))
else:
blueprint.errorhandler(code)(cls.as_view(cls.name))
else:
raise RuntimeError(
"Error views must have an `error_code` that is either an `int` or an iterable") # pragma: no cover # noqa: E501
class TemplateView(RouteView):
"""
An easy view for routes that simply render a template with no extra information.
This class is intended to be subclassed - use it as a base class for your own views, and set the class-level
attributes as appropriate. For example:
>>> class MyView(TemplateView):
... name = "my_view" # Flask internal name for this route
... path = "/my_view" # Actual URL path to reach this route
... template = "my_view.html" # Template to use
Note that this view only handles GET requests. If you need any other verbs, you can implement them yourself
or just use one of the more customizable base view classes.
"""
template = None # type: str
@classmethod
def setup(cls: "TemplateView", manager: "pysite.route_manager.RouteManager", blueprint: Blueprint):
"""
Set up the view, deferring most setup to the superclasses but checking for the template attribute.
:param manager: Instance of the current RouteManager
:param blueprint: Current Flask blueprint to register the error handler for
"""
if hasattr(super(), "setup"):
super().setup(manager, blueprint) # pragma: no cover
if not cls.template:
raise RuntimeError("Template views must have `template` defined")
def get(self, *_):
return self.render(self.template)
class RedirectView(RouteView):
"""
An easy view for routes that simply redirect to another page or view.
This class is intended to be subclassed - use it as a base class for your own views, and set the class-level
attributes as appropriate. For example:
>>> class MyView(RedirectView):
... name = "my_view" # Flask internal name for this route
... path = "/my_view" # Actual URL path to reach this route
... code = 303 # HTTP status code to use for the redirect; 303 by default
... page = "staff.index" # Page to redirect to
... kwargs = {} # Any extra keyword args to pass to the url_for call, if redirecting to another view
You can specify a full URL, including the protocol, eg "http://google.com" or a Flask internal route name,
eg "main.index". Nothing else is supported.
Note that this view only handles GET requests. If you need any other verbs, you can implement them yourself
or just use one of the more customizable base view classes.
"""
code = 303 # type: int
page = None # type: str
kwargs = {} # type: Optional[dict]
@classmethod
def setup(cls: "RedirectView", manager: "pysite.route_manager.RouteManager", blueprint: Blueprint):
"""
Set up the view, deferring most setup to the superclasses but checking for the template attribute.
:param manager: Instance of the current RouteManager
:param blueprint: Current Flask blueprint to register the error handler for
"""
if hasattr(super(), "setup"):
super().setup(manager, blueprint) # pragma: no cover
if not cls.page or not cls.code:
raise RuntimeError("Redirect views must have both `code` and `page` defined")
def get(self, *_):
if "://" in self.page:
return redirect(self.page, code=self.code)
return redirect(url_for(self.page, **self.kwargs), code=self.code)
|