Django(8):请求对象和响应对象


在Django中请求和响应都是通过内置模块进行自动传递和操作的。在客户端发送请求到服务器时,请求中所有数据会封装在 django.http.request.HttpRequest对象中,传递给视图函数的第一个参数。理论上视图处理函数都会返回一个 django.http.response.HttpResponseBase对象,包含返回给客户端的所有数据。

请求对象HttpRequest

HttpRequest的源码如下:

class HttpRequest:
    """A basic HTTP request."""

    # The encoding used in GET/POST dicts. None means use default setting.
    _encoding = None
    _upload_handlers = []

    def __init__(self):
        # WARNING: The `WSGIRequest` subclass doesn't call `super`.
        # Any variable assignment made here should also happen in
        # `WSGIRequest.__init__()`.

        self.GET = QueryDict(mutable=True)
        self.POST = QueryDict(mutable=True)
        self.COOKIES = {}
        self.META = {}
        self.FILES = MultiValueDict()

        self.path = ""
        self.path_info = ""
        self.method = None
        self.resolver_match = None
        self.content_type = None
        self.content_params = None

    def __repr__(self):
        if self.method is None or not self.get_full_path():
            return "<%s>" % self.__class__.__name__
        return "<%s: %s %r>" % (
            self.__class__.__name__,
            self.method,
            self.get_full_path(),
        )

    @cached_property
    def headers(self):
        return HttpHeaders(self.META)

    @cached_property
    def accepted_types(self):
        """Return a list of MediaType instances."""
        return parse_accept_header(self.headers.get("Accept", "*/*"))

    def accepts(self, media_type):
        return any(
            accepted_type.match(media_type) for accepted_type in self.accepted_types
        )

    def _set_content_type_params(self, meta):
        """Set content_type, content_params, and encoding."""
        self.content_type, self.content_params = parse_header_parameters(
            meta.get("CONTENT_TYPE", "")
        )
        if "charset" in self.content_params:
            try:
                codecs.lookup(self.content_params["charset"])
            except LookupError:
                pass
            else:
                self.encoding = self.content_params["charset"]

    def _get_raw_host(self):
        """
        Return the HTTP host using the environment or request headers. Skip
        allowed hosts protection, so may return an insecure host.
        """
        # We try three options, in order of decreasing preference.
        if settings.USE_X_FORWARDED_HOST and ("HTTP_X_FORWARDED_HOST" in self.META):
            host = self.META["HTTP_X_FORWARDED_HOST"]
        elif "HTTP_HOST" in self.META:
            host = self.META["HTTP_HOST"]
        else:
            # Reconstruct the host using the algorithm from PEP 333.
            host = self.META["SERVER_NAME"]
            server_port = self.get_port()
            if server_port != ("443" if self.is_secure() else "80"):
                host = "%s:%s" % (host, server_port)
        return host

    def get_host(self):
        """Return the HTTP host using the environment or request headers."""
        host = self._get_raw_host()

        # Allow variants of localhost if ALLOWED_HOSTS is empty and DEBUG=True.
        allowed_hosts = settings.ALLOWED_HOSTS
        if settings.DEBUG and not allowed_hosts:
            allowed_hosts = [".localhost", "127.0.0.1", "[::1]"]

        domain, port = split_domain_port(host)
        if domain and validate_host(domain, allowed_hosts):
            return host
        else:
            msg = "Invalid HTTP_HOST header: %r." % host
            if domain:
                msg += " You may need to add %r to ALLOWED_HOSTS." % domain
            else:
                msg += (
                    " The domain name provided is not valid according to RFC 1034/1035."
                )
            raise DisallowedHost(msg)

    def get_port(self):
        """Return the port number for the request as a string."""
        if settings.USE_X_FORWARDED_PORT and "HTTP_X_FORWARDED_PORT" in self.META:
            port = self.META["HTTP_X_FORWARDED_PORT"]
        else:
            port = self.META["SERVER_PORT"]
        return str(port)

    def get_full_path(self, force_append_slash=False):
        return self._get_full_path(self.path, force_append_slash)

    def get_full_path_info(self, force_append_slash=False):
        return self._get_full_path(self.path_info, force_append_slash)

    def _get_full_path(self, path, force_append_slash):
        # RFC 3986 requires query string arguments to be in the ASCII range.
        # Rather than crash if this doesn't happen, we encode defensively.
        return "%s%s%s" % (
            escape_uri_path(path),
            "/" if force_append_slash and not path.endswith("/") else "",
            ("?" + iri_to_uri(self.META.get("QUERY_STRING", "")))
            if self.META.get("QUERY_STRING", "")
            else "",
        )

    def get_signed_cookie(self, key, default=RAISE_ERROR, salt="", max_age=None):
        """
        Attempt to return a signed cookie. If the signature fails or the
        cookie has expired, raise an exception, unless the `default` argument
        is provided,  in which case return that value.
        """
        try:
            cookie_value = self.COOKIES[key]
        except KeyError:
            if default is not RAISE_ERROR:
                return default
            else:
                raise
        try:
            value = signing.get_cookie_signer(salt=key + salt).unsign(
                cookie_value, max_age=max_age
            )
        except signing.BadSignature:
            if default is not RAISE_ERROR:
                return default
            else:
                raise
        return value

    def build_absolute_uri(self, location=None):
        """
        Build an absolute URI from the location and the variables available in
        this request. If no ``location`` is specified, build the absolute URI
        using request.get_full_path(). If the location is absolute, convert it
        to an RFC 3987 compliant URI and return it. If location is relative or
        is scheme-relative (i.e., ``//example.com/``), urljoin() it to a base
        URL constructed from the request variables.
        """
        if location is None:
            # Make it an absolute url (but schemeless and domainless) for the
            # edge case that the path starts with '//'.
            location = "//%s" % self.get_full_path()
        else:
            # Coerce lazy locations.
            location = str(location)
        bits = urlsplit(location)
        if not (bits.scheme and bits.netloc):
            # Handle the simple, most common case. If the location is absolute
            # and a scheme or host (netloc) isn't provided, skip an expensive
            # urljoin() as long as no path segments are '.' or '..'.
            if (
                bits.path.startswith("/")
                and not bits.scheme
                and not bits.netloc
                and "/./" not in bits.path
                and "/../" not in bits.path
            ):
                # If location starts with '//' but has no netloc, reuse the
                # schema and netloc from the current request. Strip the double
                # slashes and continue as if it wasn't specified.
                if location.startswith("//"):
                    location = location[2:]
                location = self._current_scheme_host + location
            else:
                # Join the constructed URL with the provided location, which
                # allows the provided location to apply query strings to the
                # base path.
                location = urljoin(self._current_scheme_host + self.path, location)
        return iri_to_uri(location)

    @cached_property
    def _current_scheme_host(self):
        return "{}://{}".format(self.scheme, self.get_host())

    def _get_scheme(self):
        """
        Hook for subclasses like WSGIRequest to implement. Return 'http' by
        default.
        """
        return "http"

    @property
    def scheme(self):
        if settings.SECURE_PROXY_SSL_HEADER:
            try:
                header, secure_value = settings.SECURE_PROXY_SSL_HEADER
            except ValueError:
                raise ImproperlyConfigured(
                    "The SECURE_PROXY_SSL_HEADER setting must be a tuple containing "
                    "two values."
                )
            header_value = self.META.get(header)
            if header_value is not None:
                header_value, *_ = header_value.split(",", 1)
                return "https" if header_value.strip() == secure_value else "http"
        return self._get_scheme()

    def is_secure(self):
        return self.scheme == "https"

    @property
    def encoding(self):
        return self._encoding

    @encoding.setter
    def encoding(self, val):
        """
        Set the encoding used for GET/POST accesses. If the GET or POST
        dictionary has already been created, remove and recreate it on the
        next access (so that it is decoded correctly).
        """
        self._encoding = val
        if hasattr(self, "GET"):
            del self.GET
        if hasattr(self, "_post"):
            del self._post

    def _initialize_handlers(self):
        self._upload_handlers = [
            uploadhandler.load_handler(handler, self)
            for handler in settings.FILE_UPLOAD_HANDLERS
        ]

    @property
    def upload_handlers(self):
        if not self._upload_handlers:
            # If there are no upload handlers defined, initialize them from settings.
            self._initialize_handlers()
        return self._upload_handlers

    @upload_handlers.setter
    def upload_handlers(self, upload_handlers):
        if hasattr(self, "_files"):
            raise AttributeError(
                "You cannot set the upload handlers after the upload has been "
                "processed."
            )
        self._upload_handlers = upload_handlers

    def parse_file_upload(self, META, post_data):
        """Return a tuple of (POST QueryDict, FILES MultiValueDict)."""
        self.upload_handlers = ImmutableList(
            self.upload_handlers,
            warning=(
                "You cannot alter upload handlers after the upload has been "
                "processed."
            ),
        )
        parser = MultiPartParser(META, post_data, self.upload_handlers, self.encoding)
        return parser.parse()

    @property
    def body(self):
        if not hasattr(self, "_body"):
            if self._read_started:
                raise RawPostDataException(
                    "You cannot access body after reading from request's data stream"
                )

            # Limit the maximum request data size that will be handled in-memory.
            if (
                settings.DATA_UPLOAD_MAX_MEMORY_SIZE is not None
                and int(self.META.get("CONTENT_LENGTH") or 0)
                > settings.DATA_UPLOAD_MAX_MEMORY_SIZE
            ):
                raise RequestDataTooBig(
                    "Request body exceeded settings.DATA_UPLOAD_MAX_MEMORY_SIZE."
                )

            try:
                self._body = self.read()
            except OSError as e:
                raise UnreadablePostError(*e.args) from e
            self._stream = BytesIO(self._body)
        return self._body

    def _mark_post_parse_error(self):
        self._post = QueryDict()
        self._files = MultiValueDict()

    def _load_post_and_files(self):
        """Populate self._post and self._files if the content-type is a form type"""
        if self.method != "POST":
            self._post, self._files = (
                QueryDict(encoding=self._encoding),
                MultiValueDict(),
            )
            return
        if self._read_started and not hasattr(self, "_body"):
            self._mark_post_parse_error()
            return

        if self.content_type == "multipart/form-data":
            if hasattr(self, "_body"):
                # Use already read data
                data = BytesIO(self._body)
            else:
                data = self
            try:
                self._post, self._files = self.parse_file_upload(self.META, data)
            except MultiPartParserError:
                # An error occurred while parsing POST data. Since when
                # formatting the error the request handler might access
                # self.POST, set self._post and self._file to prevent
                # attempts to parse POST data again.
                self._mark_post_parse_error()
                raise
        elif self.content_type == "application/x-www-form-urlencoded":
            self._post, self._files = (
                QueryDict(self.body, encoding=self._encoding),
                MultiValueDict(),
            )
        else:
            self._post, self._files = (
                QueryDict(encoding=self._encoding),
                MultiValueDict(),
            )

    def close(self):
        if hasattr(self, "_files"):
            for f in chain.from_iterable(list_[1] for list_ in self._files.lists()):
                f.close()

    # File-like and iterator interface.
    #
    # Expects self._stream to be set to an appropriate source of bytes by
    # a corresponding request subclass (e.g. WSGIRequest).
    # Also when request data has already been read by request.POST or
    # request.body, self._stream points to a BytesIO instance
    # containing that data.

    def read(self, *args, **kwargs):
        self._read_started = True
        try:
            return self._stream.read(*args, **kwargs)
        except OSError as e:
            raise UnreadablePostError(*e.args) from e

    def readline(self, *args, **kwargs):
        self._read_started = True
        try:
            return self._stream.readline(*args, **kwargs)
        except OSError as e:
            raise UnreadablePostError(*e.args) from e

    def __iter__(self):
        return iter(self.readline, b"")

    def readlines(self):
        return list(self)

可以看到其属性比较多,其中还有@property修饰的方法属性。下面说一下常见的一些属性:

1.HttpRequest.body

原始的 HTTP 请求体作为一个字节字符串。这对于以不同方式处理非常规 HTML 表单的数据很有用:二进制图像,XML 有效负载等。对于处理传统的表单数据,使用 HttpRequest.POST

2.HttpRequest.method

代表请求中使用的 HTTP 方法的字符串,一定是大写字母。

3.HttpRequest.GET

一个类似字典的对象,包含所有给定的 HTTP GET 参数。

4.HttpRequest.POST

一个类似字典的对象,包含所有给定的 HTTP POST 参数,前提是请求包含表单数据。如果你需要访问请求中发布的原始或非表单数据,可以通过 HttpRequest.body 属性来访问。

5.HttpRequest.COOKIES

一个包含所有 cookies 的字典。键和值是字符串。

6.HttpRequest.FILES

一个类似字典的对象,包含所有上传的文件。FILES 中的每个键是 中的 name。FILES 中的每个值是一个 UploadedFile。
FILES 只有在请求方法是 POST,并且发布请求的有 enctype=“multipart/form-data” 的情况下,才会包含数据。否则,FILES 将是一个类似字典的空白对象。

7.HttpRequest.META

一个包含所有可用的 HTTP 头文件的字典。可用的头信息取决于客户端和服务器。一些可能的例子如下:

1.CONTENT_LENGTH —— 请求体的长度(字符串)。

2.CONTENT_TYPE —— 请求体的 MIME 类型。

3.HTTP_ACCEPT —— 可接受的响应内容类型。

4.HTTP_ACCEPT_ENCODING —— 可接受的响应编码。

5.HTTP_ACCEPT_LANGUAGE —— 可接受的响应语言。

6.HTTP_HOST —— 客户端发送的 HTTP 主机头。

7.HTTP_REFERER —— referrer 页面,如果有的话。

8.HTTP_USER_AGENT —— 客户端的用户代理字符串。

9.QUERY_STRING —— 查询字符串,是一个单一的(未解析的)字符串。

10.REMOTE_ADDR —— 客户机的 IP 地址。

11.REMOTE_HOST —— 客户机的主机名。

12.REMOTE_USER —— Web 服务器认证的用户,如果有的话。

13.REQUEST_METHOD —— “GET” 或 “POST” 等字符串。

14.SERVER_NAME —— 服务器的主机名。

15.SERVER_PORT —— 服务器的端口(字符串)。

请求中的任何 HTTP 头都会被转换为 META 键,方法是将所有字符转换为大写字母,用下划线代替任何连字符,并在名称前加上 HTTP_` 前缀。例如,请求头里的X-CSRFToken在META中变为HTTP_X_CSRFTOKEN。

请求数据集QuerySet

上面我们可以看到,HttpRequest对象的 self.GET 和self.POST属性会返回一个数据集对象。对于常规的GET和POST等请求,请求数据都是包含在QuerySet对象中进行传递和处理的。

QuerySet常见的属性和方法如下:

属性/方法描述
get(key, default=None)根据key获取数据集中的value
setdefault(key, default=None)设置数据集中指定key的默认值
update(key, default=None)更新数据集中指定key的数据
items(key, default=None)获取数据集中所有键值对数据
values(key, default=None)更新数据集中所有的value值
getlist(key, default=None)获取数据集中指定key的对应的多个数据
setlist(key, list_)设置数据集中指定key的值
appendlist(key, item)追加数据集中指定key的对应的列表数据
setlistdefault(key, default_list = None)设置数据集中指定key的对应的默认列表数据
lists()获取所有列表数据
dict()获取所有列表数据并转为字典输出

其中加粗的比较常用。

响应对象HttpResponse

源码如下:

import datetime
import io
import json
import mimetypes
import os
import re
import sys
import time
from email.header import Header
from http.client import responses
from urllib.parse import quote, urlparse

from django.conf import settings
from django.core import signals, signing
from django.core.exceptions import DisallowedRedirect
from django.core.serializers.json import DjangoJSONEncoder
from django.http.cookie import SimpleCookie
from django.utils import timezone
from django.utils.datastructures import CaseInsensitiveMapping
from django.utils.encoding import iri_to_uri
from django.utils.http import http_date
from django.utils.regex_helper import _lazy_re_compile

_charset_from_content_type_re = _lazy_re_compile(
    r";\s*charset=(?P<charset>[^\s;]+)", re.I
)


class ResponseHeaders(CaseInsensitiveMapping):
    def __init__(self, data):
        """
        Populate the initial data using __setitem__ to ensure values are
        correctly encoded.
        """
        self._store = {}
        if data:
            for header, value in self._unpack_items(data):
                self[header] = value

    def _convert_to_charset(self, value, charset, mime_encode=False):
        """
        Convert headers key/value to ascii/latin-1 native strings.
        `charset` must be 'ascii' or 'latin-1'. If `mime_encode` is True and
        `value` can't be represented in the given charset, apply MIME-encoding.
        """
        try:
            if isinstance(value, str):
                # Ensure string is valid in given charset
                value.encode(charset)
            elif isinstance(value, bytes):
                # Convert bytestring using given charset
                value = value.decode(charset)
            else:
                value = str(value)
                # Ensure string is valid in given charset.
                value.encode(charset)
            if "\n" in value or "\r" in value:
                raise BadHeaderError(
                    f"Header values can't contain newlines (got {value!r})"
                )
        except UnicodeError as e:
            # Encoding to a string of the specified charset failed, but we
            # don't know what type that value was, or if it contains newlines,
            # which we may need to check for before sending it to be
            # encoded for multiple character sets.
            if (isinstance(value, bytes) and (b"\n" in value or b"\r" in value)) or (
                isinstance(value, str) and ("\n" in value or "\r" in value)
            ):
                raise BadHeaderError(
                    f"Header values can't contain newlines (got {value!r})"
                ) from e
            if mime_encode:
                value = Header(value, "utf-8", maxlinelen=sys.maxsize).encode()
            else:
                e.reason += ", HTTP response headers must be in %s format" % charset
                raise
        return value

    def __delitem__(self, key):
        self.pop(key)

    def __setitem__(self, key, value):
        key = self._convert_to_charset(key, "ascii")
        value = self._convert_to_charset(value, "latin-1", mime_encode=True)
        self._store[key.lower()] = (key, value)

    def pop(self, key, default=None):
        return self._store.pop(key.lower(), default)

    def setdefault(self, key, value):
        if key not in self:
            self[key] = value


class BadHeaderError(ValueError):
    pass


class HttpResponseBase:
    """
    An HTTP response base class with dictionary-accessed headers.

    This class doesn't handle content. It should not be used directly.
    Use the HttpResponse and StreamingHttpResponse subclasses instead.
    """

    status_code = 200

    def __init__(
        self, content_type=None, status=None, reason=None, charset=None, headers=None
    ):
        self.headers = ResponseHeaders(headers)
        self._charset = charset
        if "Content-Type" not in self.headers:
            if content_type is None:
                content_type = f"text/html; charset={self.charset}"
            self.headers["Content-Type"] = content_type
        elif content_type:
            raise ValueError(
                "'headers' must not contain 'Content-Type' when the "
                "'content_type' parameter is provided."
            )
        self._resource_closers = []
        # This parameter is set by the handler. It's necessary to preserve the
        # historical behavior of request_finished.
        self._handler_class = None
        self.cookies = SimpleCookie()
        self.closed = False
        if status is not None:
            try:
                self.status_code = int(status)
            except (ValueError, TypeError):
                raise TypeError("HTTP status code must be an integer.")

            if not 100 <= self.status_code <= 599:
                raise ValueError("HTTP status code must be an integer from 100 to 599.")
        self._reason_phrase = reason

    @property
    def reason_phrase(self):
        if self._reason_phrase is not None:
            return self._reason_phrase
        # Leave self._reason_phrase unset in order to use the default
        # reason phrase for status code.
        return responses.get(self.status_code, "Unknown Status Code")

    @reason_phrase.setter
    def reason_phrase(self, value):
        self._reason_phrase = value

    @property
    def charset(self):
        if self._charset is not None:
            return self._charset
        # The Content-Type header may not yet be set, because the charset is
        # being inserted *into* it.
        if content_type := self.headers.get("Content-Type"):
            if matched := _charset_from_content_type_re.search(content_type):
                # Extract the charset and strip its double quotes.
                # Note that having parsed it from the Content-Type, we don't
                # store it back into the _charset for later intentionally, to
                # allow for the Content-Type to be switched again later.
                return matched["charset"].replace('"', "")
        return settings.DEFAULT_CHARSET

    @charset.setter
    def charset(self, value):
        self._charset = value

    def serialize_headers(self):
        """HTTP headers as a bytestring."""
        return b"\r\n".join(
            [
                key.encode("ascii") + b": " + value.encode("latin-1")
                for key, value in self.headers.items()
            ]
        )

    __bytes__ = serialize_headers

    @property
    def _content_type_for_repr(self):
        return (
            ', "%s"' % self.headers["Content-Type"]
            if "Content-Type" in self.headers
            else ""
        )

    def __setitem__(self, header, value):
        self.headers[header] = value

    def __delitem__(self, header):
        del self.headers[header]

    def __getitem__(self, header):
        return self.headers[header]

    def has_header(self, header):
        """Case-insensitive check for a header."""
        return header in self.headers

    __contains__ = has_header

    def items(self):
        return self.headers.items()

    def get(self, header, alternate=None):
        return self.headers.get(header, alternate)

    def set_cookie(
        self,
        key,
        value="",
        max_age=None,
        expires=None,
        path="/",
        domain=None,
        secure=False,
        httponly=False,
        samesite=None,
    ):
        """
        Set a cookie.

        ``expires`` can be:
        - a string in the correct format,
        - a naive ``datetime.datetime`` object in UTC,
        - an aware ``datetime.datetime`` object in any time zone.
        If it is a ``datetime.datetime`` object then calculate ``max_age``.

        ``max_age`` can be:
        - int/float specifying seconds,
        - ``datetime.timedelta`` object.
        """
        self.cookies[key] = value
        if expires is not None:
            if isinstance(expires, datetime.datetime):
                if timezone.is_naive(expires):
                    expires = timezone.make_aware(expires, datetime.timezone.utc)
                delta = expires - datetime.datetime.now(tz=datetime.timezone.utc)
                # Add one second so the date matches exactly (a fraction of
                # time gets lost between converting to a timedelta and
                # then the date string).
                delta = delta + datetime.timedelta(seconds=1)
                # Just set max_age - the max_age logic will set expires.
                expires = None
                if max_age is not None:
                    raise ValueError("'expires' and 'max_age' can't be used together.")
                max_age = max(0, delta.days * 86400 + delta.seconds)
            else:
                self.cookies[key]["expires"] = expires
        else:
            self.cookies[key]["expires"] = ""
        if max_age is not None:
            if isinstance(max_age, datetime.timedelta):
                max_age = max_age.total_seconds()
            self.cookies[key]["max-age"] = int(max_age)
            # IE requires expires, so set it if hasn't been already.
            if not expires:
                self.cookies[key]["expires"] = http_date(time.time() + max_age)
        if path is not None:
            self.cookies[key]["path"] = path
        if domain is not None:
            self.cookies[key]["domain"] = domain
        if secure:
            self.cookies[key]["secure"] = True
        if httponly:
            self.cookies[key]["httponly"] = True
        if samesite:
            if samesite.lower() not in ("lax", "none", "strict"):
                raise ValueError('samesite must be "lax", "none", or "strict".')
            self.cookies[key]["samesite"] = samesite

    def setdefault(self, key, value):
        """Set a header unless it has already been set."""
        self.headers.setdefault(key, value)

    def set_signed_cookie(self, key, value, salt="", **kwargs):
        value = signing.get_cookie_signer(salt=key + salt).sign(value)
        return self.set_cookie(key, value, **kwargs)

    def delete_cookie(self, key, path="/", domain=None, samesite=None):
        # Browsers can ignore the Set-Cookie header if the cookie doesn't use
        # the secure flag and:
        # - the cookie name starts with "__Host-" or "__Secure-", or
        # - the samesite is "none".
        secure = key.startswith(("__Secure-", "__Host-")) or (
            samesite and samesite.lower() == "none"
        )
        self.set_cookie(
            key,
            max_age=0,
            path=path,
            domain=domain,
            secure=secure,
            expires="Thu, 01 Jan 1970 00:00:00 GMT",
            samesite=samesite,
        )

    # Common methods used by subclasses

    def make_bytes(self, value):
        """Turn a value into a bytestring encoded in the output charset."""
        # Per PEP 3333, this response body must be bytes. To avoid returning
        # an instance of a subclass, this function returns `bytes(value)`.
        # This doesn't make a copy when `value` already contains bytes.

        # Handle string types -- we can't rely on force_bytes here because:
        # - Python attempts str conversion first
        # - when self._charset != 'utf-8' it re-encodes the content
        if isinstance(value, (bytes, memoryview)):
            return bytes(value)
        if isinstance(value, str):
            return bytes(value.encode(self.charset))
        # Handle non-string types.
        return str(value).encode(self.charset)

    # These methods partially implement the file-like object interface.
    # See https://docs.python.org/library/io.html#io.IOBase

    # The WSGI server must call this method upon completion of the request.
    # See http://blog.dscpl.com.au/2012/10/obligations-for-calling-close-on.html
    def close(self):
        for closer in self._resource_closers:
            try:
                closer()
            except Exception:
                pass
        # Free resources that were still referenced.
        self._resource_closers.clear()
        self.closed = True
        signals.request_finished.send(sender=self._handler_class)

    def write(self, content):
        raise OSError("This %s instance is not writable" % self.__class__.__name__)

    def flush(self):
        pass

    def tell(self):
        raise OSError(
            "This %s instance cannot tell its position" % self.__class__.__name__
        )

    # These methods partially implement a stream-like object interface.
    # See https://docs.python.org/library/io.html#io.IOBase

    def readable(self):
        return False

    def seekable(self):
        return False

    def writable(self):
        return False

    def writelines(self, lines):
        raise OSError("This %s instance is not writable" % self.__class__.__name__)


class HttpResponse(HttpResponseBase):
    """
    An HTTP response class with a string as content.

    This content can be read, appended to, or replaced.
    """

    streaming = False

    def __init__(self, content=b"", *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Content is a bytestring. See the `content` property methods.
        self.content = content

    def __repr__(self):
        return "<%(cls)s status_code=%(status_code)d%(content_type)s>" % {
            "cls": self.__class__.__name__,
            "status_code": self.status_code,
            "content_type": self._content_type_for_repr,
        }

    def serialize(self):
        """Full HTTP message, including headers, as a bytestring."""
        return self.serialize_headers() + b"\r\n\r\n" + self.content

    __bytes__ = serialize

    @property
    def content(self):
        return b"".join(self._container)

    @content.setter
    def content(self, value):
        # Consume iterators upon assignment to allow repeated iteration.
        if hasattr(value, "__iter__") and not isinstance(
            value, (bytes, memoryview, str)
        ):
            content = b"".join(self.make_bytes(chunk) for chunk in value)
            if hasattr(value, "close"):
                try:
                    value.close()
                except Exception:
                    pass
        else:
            content = self.make_bytes(value)
        # Create a list of properly encoded bytestrings to support write().
        self._container = [content]

    def __iter__(self):
        return iter(self._container)

    def write(self, content):
        self._container.append(self.make_bytes(content))

    def tell(self):
        return len(self.content)

    def getvalue(self):
        return self.content

    def writable(self):
        return True

    def writelines(self, lines):
        for line in lines:
            self.write(line)


class StreamingHttpResponse(HttpResponseBase):
    """
    A streaming HTTP response class with an iterator as content.

    This should only be iterated once, when the response is streamed to the
    client. However, it can be appended to or replaced with a new iterator
    that wraps the original content (or yields entirely new content).
    """

    streaming = True

    def __init__(self, streaming_content=(), *args, **kwargs):
        super().__init__(*args, **kwargs)
        # `streaming_content` should be an iterable of bytestrings.
        # See the `streaming_content` property methods.
        self.streaming_content = streaming_content

    def __repr__(self):
        return "<%(cls)s status_code=%(status_code)d%(content_type)s>" % {
            "cls": self.__class__.__qualname__,
            "status_code": self.status_code,
            "content_type": self._content_type_for_repr,
        }

    @property
    def content(self):
        raise AttributeError(
            "This %s instance has no `content` attribute. Use "
            "`streaming_content` instead." % self.__class__.__name__
        )

    @property
    def streaming_content(self):
        return map(self.make_bytes, self._iterator)

    @streaming_content.setter
    def streaming_content(self, value):
        self._set_streaming_content(value)

    def _set_streaming_content(self, value):
        # Ensure we can never iterate on "value" more than once.
        self._iterator = iter(value)
        if hasattr(value, "close"):
            self._resource_closers.append(value.close)

    def __iter__(self):
        return self.streaming_content

    def getvalue(self):
        return b"".join(self.streaming_content)


class FileResponse(StreamingHttpResponse):
    """
    A streaming HTTP response class optimized for files.
    """

    block_size = 4096

    def __init__(self, *args, as_attachment=False, filename="", **kwargs):
        self.as_attachment = as_attachment
        self.filename = filename
        self._no_explicit_content_type = (
            "content_type" not in kwargs or kwargs["content_type"] is None
        )
        super().__init__(*args, **kwargs)

    def _set_streaming_content(self, value):
        if not hasattr(value, "read"):
            self.file_to_stream = None
            return super()._set_streaming_content(value)

        self.file_to_stream = filelike = value
        if hasattr(filelike, "close"):
            self._resource_closers.append(filelike.close)
        value = iter(lambda: filelike.read(self.block_size), b"")
        self.set_headers(filelike)
        super()._set_streaming_content(value)

    def set_headers(self, filelike):
        """
        Set some common response headers (Content-Length, Content-Type, and
        Content-Disposition) based on the `filelike` response content.
        """
        filename = getattr(filelike, "name", "")
        filename = filename if isinstance(filename, str) else ""
        seekable = hasattr(filelike, "seek") and (
            not hasattr(filelike, "seekable") or filelike.seekable()
        )
        if hasattr(filelike, "tell"):
            if seekable:
                initial_position = filelike.tell()
                filelike.seek(0, io.SEEK_END)
                self.headers["Content-Length"] = filelike.tell() - initial_position
                filelike.seek(initial_position)
            elif hasattr(filelike, "getbuffer"):
                self.headers["Content-Length"] = (
                    filelike.getbuffer().nbytes - filelike.tell()
                )
            elif os.path.exists(filename):
                self.headers["Content-Length"] = (
                    os.path.getsize(filename) - filelike.tell()
                )
        elif seekable:
            self.headers["Content-Length"] = sum(
                iter(lambda: len(filelike.read(self.block_size)), 0)
            )
            filelike.seek(-int(self.headers["Content-Length"]), io.SEEK_END)

        filename = os.path.basename(self.filename or filename)
        if self._no_explicit_content_type:
            if filename:
                content_type, encoding = mimetypes.guess_type(filename)
                # Encoding isn't set to prevent browsers from automatically
                # uncompressing files.
                content_type = {
                    "bzip2": "application/x-bzip",
                    "gzip": "application/gzip",
                    "xz": "application/x-xz",
                }.get(encoding, content_type)
                self.headers["Content-Type"] = (
                    content_type or "application/octet-stream"
                )
            else:
                self.headers["Content-Type"] = "application/octet-stream"

        if filename:
            disposition = "attachment" if self.as_attachment else "inline"
            try:
                filename.encode("ascii")
                file_expr = 'filename="{}"'.format(
                    filename.replace("\\", "\\\\").replace('"', r"\"")
                )
            except UnicodeEncodeError:
                file_expr = "filename*=utf-8''{}".format(quote(filename))
            self.headers["Content-Disposition"] = "{}; {}".format(
                disposition, file_expr
            )
        elif self.as_attachment:
            self.headers["Content-Disposition"] = "attachment"


class HttpResponseRedirectBase(HttpResponse):
    allowed_schemes = ["http", "https", "ftp"]

    def __init__(self, redirect_to, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self["Location"] = iri_to_uri(redirect_to)
        parsed = urlparse(str(redirect_to))
        if parsed.scheme and parsed.scheme not in self.allowed_schemes:
            raise DisallowedRedirect(
                "Unsafe redirect to URL with protocol '%s'" % parsed.scheme
            )

    url = property(lambda self: self["Location"])

    def __repr__(self):
        return (
            '<%(cls)s status_code=%(status_code)d%(content_type)s, url="%(url)s">'
            % {
                "cls": self.__class__.__name__,
                "status_code": self.status_code,
                "content_type": self._content_type_for_repr,
                "url": self.url,
            }
        )


class HttpResponseRedirect(HttpResponseRedirectBase):
    status_code = 302


class HttpResponsePermanentRedirect(HttpResponseRedirectBase):
    status_code = 301


class HttpResponseNotModified(HttpResponse):
    status_code = 304

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        del self["content-type"]

    @HttpResponse.content.setter
    def content(self, value):
        if value:
            raise AttributeError(
                "You cannot set content to a 304 (Not Modified) response"
            )
        self._container = []


class HttpResponseBadRequest(HttpResponse):
    status_code = 400


class HttpResponseNotFound(HttpResponse):
    status_code = 404


class HttpResponseForbidden(HttpResponse):
    status_code = 403


class HttpResponseNotAllowed(HttpResponse):
    status_code = 405

    def __init__(self, permitted_methods, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self["Allow"] = ", ".join(permitted_methods)

    def __repr__(self):
        return "<%(cls)s [%(methods)s] status_code=%(status_code)d%(content_type)s>" % {
            "cls": self.__class__.__name__,
            "status_code": self.status_code,
            "content_type": self._content_type_for_repr,
            "methods": self["Allow"],
        }


class HttpResponseGone(HttpResponse):
    status_code = 410


class HttpResponseServerError(HttpResponse):
    status_code = 500


class Http404(Exception):
    pass


class JsonResponse(HttpResponse):
    """
    An HTTP response class that consumes data to be serialized to JSON.

    :param data: Data to be dumped into json. By default only ``dict`` objects
      are allowed to be passed due to a security flaw before ECMAScript 5. See
      the ``safe`` parameter for more information.
    :param encoder: Should be a json encoder class. Defaults to
      ``django.core.serializers.json.DjangoJSONEncoder``.
    :param safe: Controls if only ``dict`` objects may be serialized. Defaults
      to ``True``.
    :param json_dumps_params: A dictionary of kwargs passed to json.dumps().
    """

    def __init__(
        self,
        data,
        encoder=DjangoJSONEncoder,
        safe=True,
        json_dumps_params=None,
        **kwargs,
    ):
        if safe and not isinstance(data, dict):
            raise TypeError(
                "In order to allow non-dict objects to be serialized set the "
                "safe parameter to False."
            )
        if json_dumps_params is None:
            json_dumps_params = {}
        kwargs.setdefault("content_type", "application/json")
        data = json.dumps(data, cls=encoder, **json_dumps_params)
        super().__init__(content=data, **kwargs)

看源码可以看到,HttpResponse继承HttpResponseBase。

主要属性和方法

HttpResponse的主要属性和方法如下:

属性/方法描述
has_header(header)是否忽略请求头的大小写限制
setdefault(header, value)设置响应头数据
set_cookie(key, value, **kwargs)设置cookie数据
set_signed_cookie(key, value, **kwargs)设置安全的cookie数据
delete_cookie(key, path=‘/’, domain=None)删除指定key的cookie数据
write(content)向客户端写入返回的数据,可以追加
flush(h)刷新响应数据,数据全部返回

HttpResponse子类扩展

同时,针对响应对象,根据不同的场景,Django封装了一些子类扩展其功能,如下表:

子类类型描述
HttpResponseRedirect请求重定向,状态码302
HttpResponsePermanentRedirect请求永久重定向,状态码301
HttpResponseNotModified请求资源未修改,状态码304
HttpResponseBadRequest错误的请求,状态码400
HttpResponseNotFound请求资源未找到,状态码404
HttpResponseForbidden请求资源禁止访问,状态码403
HttpResponseNotAllowed请求不允许,状态码405
HttpResponseGone请求资源已失效,状态码410
HttpResponseServerError服务器内部错误,状态码500

特殊响应类型

此外,Django还提供了一些特定场景下的特殊响应类型

响应类型描述
JsonResponse返回json数据
StreamingHttpResponse返回流式数据,如用于大文件的返回
FileResponse返回文件数据

实际案例

以我们前的博客项目为例,继续完善我们的代码。

1.完善注册登录功能

在注册和登录函数上进行访问请求的限制,只允许GET和POST请求。

重构用户子项目的视图处理模块personal_blog/author/views.py,代码如下:

from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpResponse
from django.shortcuts import render
from django.views.decorators.http import require_http_methods

from .models import Author


@require_http_methods(['GET', 'POST'])
def author_register(request: WSGIRequest) -> HttpResponse:
    """作者注册"""
    if request.method == "GET":
        return render(request, 'author/register.html', {})
    elif request.method == "POST":
        # 获取前端传递的数据
        username = request.POST.get('username')
        password = request.POST.get('password')
        realname = request.POST.get('realname')

        # 判断账号是否注册过
        authors = Author.objects.filter(username=username)
        if len(authors) > 0:
            return render(request, 'author/register.html',
                          {'msg_code': 0, 'msg_info': '账号已经存在,请使用其他账号注册'})

        # 创建作者对象
        author = Author(username=username, password=password, realname=realname)
        author.save()

        # 返回登录页
        return render(request, 'author/login.html',
                      {'msg_code': 0, 'msg_info': '账号注册成功'})


@require_http_methods(['GET', 'POST'])
def author_login(request: WSGIRequest) -> HttpResponse:
    """作者登录"""

    if request.method == "GET":
        return render(request, 'author/register.html', {})
    else:
        username = request.POST.get('username')
        password = request.POST.get('password')

        # 查询是否存在该用户
        try:
            author = Author.objects.get(username=username, password=password)
            # 登录成功,记录登录状态
            request.session['author'] = author
            # 跳首页
            print("----", request.session)
            return render(request, 'author/index.html',
                          {'msg_code': 0, 'msg_info': '登录成功'})
        except:
            # 登录失败
            return render(request, 'author/login.html',
                          {'msg_code': -1, 'msg_info': '账号或密码错误,请重新登录'})

2.完善博客首页

用户登录后会跳转到博客首页。

把首页文件index.html移到很目录下的静态文件下下personal_blog/html_file/index,html,并完善登录的视图函数,修改返回主页路径,代码如下:

@require_http_methods(['GET', 'POST'])
def author_login(request: WSGIRequest) -> HttpResponse:
    """作者登录"""

    if request.method == "GET":
        return render(request, 'author/register.html', {})
    else:
        username = request.POST.get('username')
        password = request.POST.get('password')

        # 查询是否存在该用户
        try:
            author = Author.objects.get(username=username, password=password)
            # 登录成功,记录登录状态
            request.session['author'] = author
            # 跳首页,静态文件的路径在配置文件指定后,可以不写绝对路径
            print("----", request.session)
            return render(request, 'index.html',
                          {'msg_code': 0, 'msg_info': '登录成功'})
        except:
            # 登录失败
            return render(request, 'author/login.html',
                          {'msg_code': -1, 'msg_info': '账号或密码错误,请重新登录'})

3.完善查询所有文章的功能

用户登录后,可以查询博客中所有用户发表的所有文章,文章默认按照发表时间降序排列。

首先完善文章子项目中的models.py模块的Article模型的Meta信息:

class Article(models.Model):
    """文章类型"""
    ...

    class Meta:
        # 数据排序
        ordering = ['-pub_time', 'id']
        # 类型展示提示信息
        verbose_name = '文章'
        verbose_name_plural = verbose_name
        # 添加索引支持
        indexes = [models.Index(fields=['title'])]

然后在文章子目录下建存放html网页的文件夹,创建展示所有文章的html网页,编写文件personal_blog/article/templates/article/articles.html,代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>所有文章列表</title>
</head>
<body>
    <h1>用户查询所有文章列表</h1>
    <div>要查询的文章列表</div>
</body>
</html>

然后完善查询所有文章的视图函数,编写文件personal_blog/article/views.py,修改article_list视图代码如下:

@gzip_page
def article_list(request):
    """查询指文章列表"""
    articles = get_list_or_404(Article)
    # 指定展示页面为article/articles.html
    # 同时返回自定义操作结果错误码res_code和res_msg,以及展示的文章数据articles
    return render(request, 'article/articles.html',
                  {'res_code': '200',
                   'res_msg': '查看所有文章',
                   'articles': articles})

然后添加路由,编写文件personal_blog/article/urls.py,代码如下:

from django.urls import path, re_path
from . import views

# 路由模块名称
app_name = 'article'

# 添加路由配置
urlpatterns = [
    path('article_list/', views.article_list, name='article_list'),
    ...
]

4.完善查询指定编号的文章功能

创建静态文件personal_blog/article/templates/article/article.html,代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>所有文章详情</title>
</head>
<body>
    <h1>查询指定文章详情</h1>
    <div>展示文章信息: {{ article.title }}</div>
</body>
</html>

编写文件personal_blog/article/views.py,完善单篇文章详情视图:

@gzip_page
def article_detail(request, article_id):
    """查询指定编号的文章,article_id变量会接受路由中传递的数据"""
    print("查询指定编号的文章,编号:%s" % article_id)
    article = get_object_or_404(Article, pk=article_id)

    return render(request, 'article/article.html',
                  {'res_code': '200',
                   'res_msg': '查看文章详情',
                   'article': article})

添加路由,编写文件personal_blog/article/urls.py,代码如下:

from django.urls import path, re_path
from . import views

# 路由模块名称
app_name = 'article'

# 添加路由配置
urlpatterns = [
    ...
    re_path('^articles/<int:article_id>/', views.article_detail, name='article_detail'),
]

查询所有文章,完善文章的查询链接功能,代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>所有文章列表</title>
</head>
<body>
    <h1>用户查询所有文章列表</h1>
    <div>要查询的文章列表</div>
     <!-- articles.0表示获取从后台传递的articles列表中的第一项文章对象数据,id是文章对象的编号属性 -->
    <a href="/article/{{articles.0.id}}/detail/">查看第一篇文章详情</a>
</body>
</html>

5.完善查询作者的所有文章功能

静态页面还是使用前面文章列表的articles.html。

完善personal_blog/article/views.py中articles_author视图函数,代码如下:

@gzip_page
def articles_author(request, author_id):
    '''查询指定作者的所有文章'''
    # 查询得到作者对象
    author = get_object_or_404(Author, pk=author_id)
    # 查询作者的所有文章
    articles = author.article_set.all()
    return render(request, 'article/articles.html',
                  {'res_code': '200',
                   'res_msg': '查看作者的所有文章',
                   'articles': articles,
                   'f_index': 'none', 'f_main': 'none', 'f_message': 'none', 'f_article': 'active'})

完善路由如下:

from django.urls import path, re_path
from . import views

# 路由模块名称
app_name = 'article'

# 添加路由配置
urlpatterns = [
    ...
    # 查询指定作者的文章
    path('<uuid:author_id>/', views.articles_author, name='articles_author'),
]

打开博客首页文件personal_blog/html_file/index.html,添加查询作者文章的链接:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>博客首页</title>
</head>
<body>
    <h1>博客首页</h1>
    <h3><a href="/article/{{request.session.author.id}}/">查看作者文章</a></h3>
    {{ message }}
</body>
</html>

还有其他功能完善这里不再展示。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ethan-running

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值