1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/WebStack/Django.py Sat Jan 14 01:52:43 2006 +0000
1.3 @@ -0,0 +1,471 @@
1.4 +#!/usr/bin/env python
1.5 +
1.6 +"""
1.7 +Django classes.
1.8 +
1.9 +Copyright (C) 2006 Paul Boddie <paul@boddie.org.uk>
1.10 +
1.11 +This library is free software; you can redistribute it and/or
1.12 +modify it under the terms of the GNU Lesser General Public
1.13 +License as published by the Free Software Foundation; either
1.14 +version 2.1 of the License, or (at your option) any later version.
1.15 +
1.16 +This library is distributed in the hope that it will be useful,
1.17 +but WITHOUT ANY WARRANTY; without even the implied warranty of
1.18 +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
1.19 +Lesser General Public License for more details.
1.20 +
1.21 +You should have received a copy of the GNU Lesser General Public
1.22 +License along with this library; if not, write to the Free Software
1.23 +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
1.24 +"""
1.25 +
1.26 +import WebStack.Generic
1.27 +from WebStack.Helpers import Environment
1.28 +from WebStack.Helpers.Request import decode_value, FileContent
1.29 +from WebStack.Helpers.Response import ConvertingStream
1.30 +from django.utils.httpwrappers import HttpResponse
1.31 +from StringIO import StringIO
1.32 +
1.33 +class Transaction(WebStack.Generic.Transaction):
1.34 +
1.35 + """
1.36 + Django transaction interface.
1.37 + """
1.38 +
1.39 + def __init__(self, request):
1.40 +
1.41 + """
1.42 + Initialise the transaction with the Django 'request' object.
1.43 + """
1.44 +
1.45 + self.request = request
1.46 +
1.47 + # Attributes which may be changed later.
1.48 +
1.49 + self.content_type = None
1.50 +
1.51 + # The response is created here but must be modified later.
1.52 + # NOTE: It is unfortunate that Django wants to initialise the response
1.53 + # NOTE: with the content type immediately.
1.54 +
1.55 + self.response = HttpResponse()
1.56 + self.content = StringIO()
1.57 +
1.58 + def commit(self):
1.59 +
1.60 + "Commit the transaction by finishing some things off."
1.61 +
1.62 + self.content.seek(0)
1.63 + self.response.content = self.content.read()
1.64 +
1.65 + # Server-related methods.
1.66 +
1.67 + def get_server_name(self):
1.68 +
1.69 + "Returns the server name."
1.70 +
1.71 + return self.request.META.get("SERVER_NAME")
1.72 +
1.73 + def get_server_port(self):
1.74 +
1.75 + "Returns the server port as a string."
1.76 +
1.77 + return self.request.META.get("SERVER_PORT")
1.78 +
1.79 + # Request-related methods.
1.80 +
1.81 + def get_request_stream(self):
1.82 +
1.83 + """
1.84 + Returns the request stream for the transaction.
1.85 + """
1.86 +
1.87 + # Unfortunately, we get given a string from Django. Thus, we need to
1.88 + # create a stream around that string.
1.89 +
1.90 + return StringIO(self.request.raw_post_data)
1.91 +
1.92 + def get_request_method(self):
1.93 +
1.94 + """
1.95 + Returns the request method.
1.96 + """
1.97 +
1.98 + return self.request.META.get("REQUEST_METHOD")
1.99 +
1.100 + def get_headers(self):
1.101 +
1.102 + """
1.103 + Returns all request headers as a dictionary-like object mapping header
1.104 + names to values.
1.105 + """
1.106 +
1.107 + return Environment.get_headers(self.request.META)
1.108 +
1.109 + def get_header_values(self, key):
1.110 +
1.111 + """
1.112 + Returns a list of all request header values associated with the given
1.113 + 'key'. Note that according to RFC 2616, 'key' is treated as a
1.114 + case-insensitive string.
1.115 + """
1.116 +
1.117 + return self.convert_to_list(self.get_headers().get(key))
1.118 +
1.119 + def get_content_type(self):
1.120 +
1.121 + """
1.122 + Returns the content type specified on the request, along with the
1.123 + charset employed.
1.124 + """
1.125 +
1.126 + return self.parse_content_type(self.request.META.get("CONTENT_TYPE"))
1.127 +
1.128 + def get_content_charsets(self):
1.129 +
1.130 + """
1.131 + Returns the character set preferences.
1.132 +
1.133 + NOTE: Not decently supported.
1.134 + """
1.135 +
1.136 + return self.parse_content_preferences(None)
1.137 +
1.138 + def get_content_languages(self):
1.139 +
1.140 + """
1.141 + Returns extracted language information from the transaction.
1.142 +
1.143 + NOTE: Not decently supported.
1.144 + """
1.145 +
1.146 + return self.parse_content_preferences(None)
1.147 +
1.148 + def get_path(self, encoding=None):
1.149 +
1.150 + """
1.151 + Returns the entire path from the request as a Unicode object. Any "URL
1.152 + encoded" character values in the part of the path before the query
1.153 + string will be decoded and presented as genuine characters; the query
1.154 + string will remain "URL encoded", however.
1.155 +
1.156 + If the optional 'encoding' is set, use that in preference to the default
1.157 + encoding to convert the path into a form not containing "URL encoded"
1.158 + character values.
1.159 + """
1.160 +
1.161 + return decode_value(self.request.get_full_path(), encoding)
1.162 +
1.163 + def get_path_without_query(self, encoding=None):
1.164 +
1.165 + """
1.166 + Returns the entire path from the request minus the query string as a
1.167 + Unicode object containing genuine characters (as opposed to "URL
1.168 + encoded" character values).
1.169 +
1.170 + If the optional 'encoding' is set, use that in preference to the default
1.171 + encoding to convert the path into a form not containing "URL encoded"
1.172 + character values.
1.173 + """
1.174 +
1.175 + path = self.get_path(encoding)
1.176 + return path.split("?")[0]
1.177 +
1.178 + def get_path_info(self, encoding=None):
1.179 +
1.180 + """
1.181 + Returns the "path info" (the part of the URL after the resource name
1.182 + handling the current request) from the request as a Unicode object
1.183 + containing genuine characters (as opposed to "URL encoded" character
1.184 + values).
1.185 +
1.186 + If the optional 'encoding' is set, use that in preference to the default
1.187 + encoding to convert the path into a form not containing "URL encoded"
1.188 + character values.
1.189 + """
1.190 +
1.191 + path_info = self.request.META.get("PATH_INFO") or ""
1.192 + return decode_value(path_info, encoding)
1.193 +
1.194 + def get_query_string(self):
1.195 +
1.196 + """
1.197 + Returns the query string from the path in the request.
1.198 + """
1.199 +
1.200 + return self.request.META.get("QUERY_STRING") or ""
1.201 +
1.202 + # Higher level request-related methods.
1.203 +
1.204 + def get_fields_from_path(self, encoding=None):
1.205 +
1.206 + """
1.207 + Extracts fields (or request parameters) from the path specified in the
1.208 + transaction. The underlying framework may refuse to supply fields from
1.209 + the path if handling a POST transaction. The optional 'encoding'
1.210 + parameter specifies the character encoding of the query string for cases
1.211 + where the default encoding is to be overridden.
1.212 +
1.213 + Returns a dictionary mapping field names to lists of values (even if a
1.214 + single value is associated with any given field name).
1.215 + """
1.216 +
1.217 + return self._get_fields(self.request.GET, encoding)
1.218 +
1.219 + def get_fields_from_body(self, encoding=None):
1.220 +
1.221 + """
1.222 + Extracts fields (or request parameters) from the message body in the
1.223 + transaction. The optional 'encoding' parameter specifies the character
1.224 + encoding of the message body for cases where no such information is
1.225 + available, but where the default encoding is to be overridden.
1.226 +
1.227 + Returns a dictionary mapping field names to lists of values (even if a
1.228 + single value is associated with any given field name). Each value is
1.229 + either a Unicode object (representing a simple form field, for example)
1.230 + or a WebStack.Helpers.Request.FileContent object (representing a file
1.231 + upload form field).
1.232 + """
1.233 +
1.234 + fields = {}
1.235 + self._update_fields(fields, self._get_fields(self.request.POST, encoding))
1.236 + self._update_fields(fields, self._get_files())
1.237 + return fields
1.238 +
1.239 + def _get_fields(self, source, encoding=None):
1.240 + fields = {}
1.241 + for name in source.keys():
1.242 + name = decode_value(name, encoding)
1.243 + fields[name] = []
1.244 + for value in source.getlist(name):
1.245 + value = decode_value(value, encoding)
1.246 + fields[name].append(value)
1.247 + return fields
1.248 +
1.249 + def _get_files(self):
1.250 + files = {}
1.251 + for name, file in self.request.FILES.items():
1.252 + files[name] = [FileContent(file.get("content", ""), {
1.253 + "Content-Type" : file.get("content-type", ""),
1.254 + "Content-Disposition" : "%s; filename=%s" % (name, file.get("filename", ""))
1.255 + })]
1.256 + return files
1.257 +
1.258 + def get_fields(self, encoding=None):
1.259 +
1.260 + """
1.261 + Extracts fields (or request parameters) from both the path specified in
1.262 + the transaction as well as the message body. The optional 'encoding'
1.263 + parameter specifies the character encoding of the message body for cases
1.264 + where no such information is available, but where the default encoding
1.265 + is to be overridden.
1.266 +
1.267 + Returns a dictionary mapping field names to lists of values (even if a
1.268 + single value is associated with any given field name). Each value is
1.269 + either a Unicode object (representing a simple form field, for example)
1.270 + or a WebStack.Helpers.Request.FileContent object (representing a file
1.271 + upload form field).
1.272 +
1.273 + Where a given field name is used in both the path and message body to
1.274 + specify values, the values from both sources will be combined into a
1.275 + single list associated with that field name.
1.276 + """
1.277 +
1.278 + fields = {}
1.279 + fields.update(self.get_fields_from_path(encoding))
1.280 + self._update_fields(fields, self.get_fields_from_body(encoding))
1.281 + return fields
1.282 +
1.283 + def _update_fields(self, fields, new_fields):
1.284 + for name, values in new_fields.items():
1.285 + if not fields.has_key(name):
1.286 + fields[name] = values
1.287 + else:
1.288 + fields[name] += values
1.289 +
1.290 + def get_user(self):
1.291 +
1.292 + """
1.293 + Extracts user information from the transaction.
1.294 +
1.295 + Returns a username as a string or None if no user is defined.
1.296 + """
1.297 +
1.298 + if self.user is not None:
1.299 + return self.user
1.300 +
1.301 + if hasattr(self.request.user, "username"):
1.302 + return self.request.user.username
1.303 + else:
1.304 + return None
1.305 +
1.306 + def get_cookies(self):
1.307 +
1.308 + """
1.309 + Obtains cookie information from the request.
1.310 +
1.311 + Returns a dictionary mapping cookie names to cookie objects.
1.312 + """
1.313 +
1.314 + return self.process_cookies(self.request.COOKIES, using_strings=1)
1.315 +
1.316 + def get_cookie(self, cookie_name):
1.317 +
1.318 + """
1.319 + Obtains cookie information from the request.
1.320 +
1.321 + Returns a cookie object for the given 'cookie_name' or None if no such
1.322 + cookie exists.
1.323 + """
1.324 +
1.325 + value = self.request.COOKIES.get(self.encode_cookie_value(cookie_name))
1.326 + if value is not None:
1.327 + return Cookie(cookie_name, self.decode_cookie_value(value))
1.328 + else:
1.329 + return None
1.330 +
1.331 + # Response-related methods.
1.332 +
1.333 + def get_response_stream(self):
1.334 +
1.335 + """
1.336 + Returns the response stream for the transaction.
1.337 + """
1.338 +
1.339 + # Unicode can upset this operation. Using either the specified charset
1.340 + # or a default encoding.
1.341 +
1.342 + encoding = self.get_response_stream_encoding()
1.343 + return ConvertingStream(self.content, encoding)
1.344 +
1.345 + def get_response_stream_encoding(self):
1.346 +
1.347 + """
1.348 + Returns the response stream encoding.
1.349 + """
1.350 +
1.351 + if self.content_type:
1.352 + encoding = self.content_type.charset
1.353 + else:
1.354 + encoding = None
1.355 + return encoding or self.default_charset
1.356 +
1.357 + def get_response_code(self):
1.358 +
1.359 + """
1.360 + Get the response code associated with the transaction. If no response
1.361 + code is defined, None is returned.
1.362 + """
1.363 +
1.364 + return self.response.status_code
1.365 +
1.366 + def set_response_code(self, response_code):
1.367 +
1.368 + """
1.369 + Set the 'response_code' using a numeric constant defined in the HTTP
1.370 + specification.
1.371 + """
1.372 +
1.373 + self.response.status_code = response_code
1.374 +
1.375 + def set_header_value(self, header, value):
1.376 +
1.377 + """
1.378 + Set the HTTP 'header' with the given 'value'.
1.379 + """
1.380 +
1.381 + self.response.headers[header] = value
1.382 +
1.383 + def set_content_type(self, content_type):
1.384 +
1.385 + """
1.386 + Sets the 'content_type' for the response.
1.387 + """
1.388 +
1.389 + self.content_type = content_type
1.390 + self.response.headers["Content-Type"] = str(content_type)
1.391 +
1.392 + # Higher level response-related methods.
1.393 +
1.394 + def set_cookie(self, cookie):
1.395 +
1.396 + """
1.397 + Stores the given 'cookie' object in the response.
1.398 + """
1.399 +
1.400 + self.set_cookie_value(cookie.name, cookie.value)
1.401 +
1.402 + def set_cookie_value(self, name, value, path=None, expires=None):
1.403 +
1.404 + """
1.405 + Stores a cookie with the given 'name' and 'value' in the response.
1.406 +
1.407 + The optional 'path' is a string which specifies the scope of the cookie,
1.408 + and the optional 'expires' parameter is a value compatible with the
1.409 + time.time function, and indicates the expiry date/time of the cookie.
1.410 + """
1.411 +
1.412 + self.response.set_cookie(self.encode_cookie_value(name), self.encode_cookie_value(value), path=path, expires=expires)
1.413 +
1.414 + def delete_cookie(self, cookie_name):
1.415 +
1.416 + """
1.417 + Adds to the response a request that the cookie with the given
1.418 + 'cookie_name' be deleted/discarded by the client.
1.419 + """
1.420 +
1.421 + #self.response.delete_cookie(self.encode_cookie_value(cookie_name))
1.422 +
1.423 + # Create a special cookie, given that we do not know whether the browser
1.424 + # has been sent the cookie or not.
1.425 + # NOTE: Magic discovered in Webware.
1.426 +
1.427 + name = self.encode_cookie_value(cookie_name)
1.428 + self.response.set_cookie(name, "", path="/", expires=0, max_age=0)
1.429 +
1.430 + # Session-related methods.
1.431 +
1.432 + def get_session(self, create=1):
1.433 +
1.434 + """
1.435 + Gets a session corresponding to an identifier supplied in the
1.436 + transaction.
1.437 +
1.438 + If no session has yet been established according to information
1.439 + provided in the transaction then the optional 'create' parameter
1.440 + determines whether a new session will be established.
1.441 +
1.442 + Where no session has been established and where 'create' is set to 0
1.443 + then None is returned. In all other cases, a session object is created
1.444 + (where appropriate) and returned.
1.445 + """
1.446 +
1.447 + # NOTE: Dubious access to a more dictionary-like object.
1.448 +
1.449 + if create:
1.450 + self.request.session["_hack"] = "created"
1.451 + return Session(self.request.session)
1.452 +
1.453 + def expire_session(self):
1.454 +
1.455 + """
1.456 + Expires any session established according to information provided in the
1.457 + transaction.
1.458 + """
1.459 +
1.460 + # NOTE: Not trivially supported!
1.461 +
1.462 +class Session:
1.463 + def __init__(self, session):
1.464 + self.session = session
1.465 + def __getattr__(self, name):
1.466 + return getattr(self.session, name)
1.467 + def keys(self):
1.468 + return self.session._session.keys()
1.469 + def values(self):
1.470 + return self.session._session.values()
1.471 + def items(self):
1.472 + return self.session._session.items()
1.473 +
1.474 +# vim: tabstop=4 expandtab shiftwidth=4