pax_global_header 0000666 0000000 0000000 00000000064 14100726230 0014505 g ustar 00root root 0000000 0000000 52 comment=0328b9d7a1250ce539bb7b5bac54cdd1db5690c5
drf-extensions-0.7.1/ 0000775 0000000 0000000 00000000000 14100726230 0014462 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/.gitignore 0000664 0000000 0000000 00000000110 14100726230 0016442 0 ustar 00root root 0000000 0000000 __pycache__/
*.pyc
*.egg-info
.tox
*.egg
.idea
env
build
dist
.DS_Store
drf-extensions-0.7.1/.travis.yml 0000664 0000000 0000000 00000000256 14100726230 0016576 0 ustar 00root root 0000000 0000000 language: python
cache: pip
dist: bionic
sudo: false
arch:
- amd64
- ppc64le
python:
- 3.6
- 3.7
- 3.8
install:
- pip install tox tox-travis
script:
- tox -r
drf-extensions-0.7.1/AUTHORS.md 0000664 0000000 0000000 00000000346 14100726230 0016134 0 ustar 00root root 0000000 0000000 ## Original Author
---------------
Gennady Chibisov https://github.com/chibisov
## Core maintainer
Asif Saif Uddin https://github.com/auvipy
## Contributors
------------
Luke Murphy https://github.com/lwm
drf-extensions-0.7.1/GNUmakefile 0000664 0000000 0000000 00000000365 14100726230 0016540 0 ustar 00root root 0000000 0000000 build_docs:
PYTHONIOENCODING=utf-8 python docs/backdoc.py --title "Django Rest Framework extensions documentation" < docs/index.md > docs/index.html
watch_docs:
make build_docs
watchmedo shell-command -p "*.md" -R -c "make build_docs" docs/
drf-extensions-0.7.1/LICENSE 0000664 0000000 0000000 00000002074 14100726230 0015472 0 ustar 00root root 0000000 0000000 The MIT License (MIT)
Copyright (c) 2013 Gennady Chibisov.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
drf-extensions-0.7.1/MANIFEST.in 0000664 0000000 0000000 00000000306 14100726230 0016217 0 ustar 00root root 0000000 0000000 include LICENSE
include README.md
include tox.ini
recursive-include docs *.md *.html *.txt *.py
recursive-include tests_app requirements.txt *.py
recursive-exclude * __pycache__
global-exclude *pyc
drf-extensions-0.7.1/README.md 0000664 0000000 0000000 00000014263 14100726230 0015747 0 ustar 00root root 0000000 0000000 ## Django REST Framework extensions
DRF-extensions is a collection of custom extensions for [Django REST Framework](https://github.com/tomchristie/django-rest-framework)
Full documentation for project is available at [http://chibisov.github.io/drf-extensions/docs](http://chibisov.github.io/drf-extensions/docs)
[](https://travis-ci.org/chibisov/drf-extensions)
[](#backers) [](#sponsors) [](https://pypi.python.org/pypi/drf-extensions)
### Sponsor
[Tidelift gives software development teams a single source for purchasing and maintaining their software, with professional grade assurances from the experts who know it best, while seamlessly integrating with existing tools.](https://tidelift.com/subscription/pkg/pypi-drf-extensions?utm_source=pypi-drf-extensions&utm_medium=referral&utm_campaign=readme)
## Requirements
* Tested for Python 3.6, 3.7 and 3.8
* Tested for Django Rest Framework 3.12
* Tested for Django 2.2 to 3.2
* Tested for django-filter 2.1.0
## Installation:
pip3 install drf-extensions
or from github
pip3 install https://github.com/chibisov/drf-extensions/archive/master.zip
## Some features
* DetailSerializerMixin
* Caching
* Conditional requests
* Customizable key construction for caching and conditional requests
* Nested routes
* Bulk operations
Read more in [documentation](http://chibisov.github.io/drf-extensions/docs)
## Development
Running the tests:
$ pip3 install tox
$ tox -- tests_app
Running test for exact environment:
$ tox -e py38 -- tests_app
Recreate envs before running tests:
$ tox --recreate -- tests_app
Pass custom arguments:
$ tox -- tests_app --verbosity=3
Run with pdb support:
$ tox -- tests_app --processes=0 --nocapture
Run exact TestCase:
$ tox -- tests_app.tests.unit.mixins.tests:DetailSerializerMixinTest_serializer_detail_class
Run tests from exact module:
$ tox -- tests_app.tests.unit.mixins.tests
Build docs:
$ make build_docs
Automatically build docs by watching changes:
$ pip install watchdog
$ make watch_docs
## Developing new features
Every new feature should be:
* Documented
* Tested
* Implemented
* Pushed to main repository
### How to write documentation
When new feature implementation starts you should place it into `development version` pull. Add `Development version`
section to `Release notes` and describe every new feature in it. Use `#anchors` to facilitate navigation.
Every feature should have title and information that it was implemented in current development version.
For example if we've just implemented `Usage of the specific cache`:
...
#### Usage of the specific cache
*New in DRF-extensions development version*
`@cache_response` can also take...
...
### Release notes
...
#### Development version
* Added ability to [use a specific cache](#usage-of-the-specific-cache) for `@cache_response` decorator
## Publishing new releases
Increment version in `rest_framework_extensions/__init__.py`. For example:
__version__ = '0.2.2' # from 0.2.1
Move to new version section all release notes in documentation.
Add date for release note section.
Replace in documentation all `New in DRF-extensions development version` notes to `New in DRF-extensions 0.2.2`.
Rebuild documentation.
Run tests.
Commit changes with message "Version 0.2.2"
Add new tag version for commit:
$ git tag 0.2.2
Push to master with tags:
$ git push origin master --tags
Don't forget to merge `master` to `gh-pages` branch and push to origin:
$ git co gh-pages
$ git merge --no-ff master
$ git push origin gh-pages
Publish to pypi:
$ python setup.py publish
## Contributors
This project exists thanks to all the people who contribute.
## Backers
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/drf-extensions#backer)]
## Sponsors
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/drf-extensions#sponsor)]
drf-extensions-0.7.1/docs/ 0000775 0000000 0000000 00000000000 14100726230 0015412 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/docs/backdoc.py 0000664 0000000 0000000 00000616015 14100726230 0017363 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
"""
Backdoc is a tool for backbone-like documentation generation.
Backdoc main goal is to help to generate one page documentation from one markdown source file.
https://github.com/chibisov/backdoc
"""
import sys
import argparse
# Copyright (c) 2012 Trent Mick.
# Copyright (c) 2007-2008 ActiveState Corp.
# License: MIT (http://www.opensource.org/licenses/mit-license.php)
r"""A fast and complete Python implementation of Markdown.
[from http://daringfireball.net/projects/markdown/]
> Markdown is a text-to-HTML filter; it translates an easy-to-read /
> easy-to-write structured text format into HTML. Markdown's text
> format is most similar to that of plain text email, and supports
> features such as headers, *emphasis*, code blocks, blockquotes, and
> links.
>
> Markdown's syntax is designed not as a generic markup language, but
> specifically to serve as a front-end to (X)HTML. You can use span-level
> HTML tags anywhere in a Markdown document, and you can use block level
> HTML tags (like
tags.
"""
yield 0, ""
for tup in inner:
yield tup
yield 0, "
"
def wrap(self, source, outfile):
"""Return the source with a code, pre, and div."""
return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
formatter_opts.setdefault("cssclass", "codehilite")
formatter = HtmlCodeFormatter(**formatter_opts)
return pygments.highlight(codeblock, lexer, formatter)
def _code_block_sub(self, match, is_fenced_code_block=False):
lexer_name = None
if is_fenced_code_block:
lexer_name = match.group(1)
if lexer_name:
formatter_opts = self.extras['fenced-code-blocks'] or {}
codeblock = match.group(2)
codeblock = codeblock[:-1] # drop one trailing newline
else:
codeblock = match.group(1)
codeblock = self._outdent(codeblock)
codeblock = self._detab(codeblock)
codeblock = codeblock.lstrip('\n') # trim leading newlines
codeblock = codeblock.rstrip() # trim trailing whitespace
# Note: "code-color" extra is DEPRECATED.
if "code-color" in self.extras and codeblock.startswith(":::"):
lexer_name, rest = codeblock.split('\n', 1)
lexer_name = lexer_name[3:].strip()
codeblock = rest.lstrip("\n") # Remove lexer declaration line.
formatter_opts = self.extras['code-color'] or {}
if lexer_name:
lexer = self._get_pygments_lexer(lexer_name)
if lexer:
colored = self._color_with_pygments(codeblock, lexer,
**formatter_opts)
return "\n\n%s\n\n" % colored
codeblock = self._encode_code(codeblock)
pre_class_str = self._html_class_str_from_tag("pre")
code_class_str = self._html_class_str_from_tag("code")
return "\n\n%s\n
\n\n" % (
pre_class_str, code_class_str, codeblock)
def _html_class_str_from_tag(self, tag):
"""Get the appropriate ' class="..."' string (note the leading
space), if any, for the given tag.
"""
if "html-classes" not in self.extras:
return ""
try:
html_classes_from_tag = self.extras["html-classes"]
except TypeError:
return ""
else:
if tag in html_classes_from_tag:
return ' class="%s"' % html_classes_from_tag[tag]
return ""
def _do_code_blocks(self, text):
"""Process Markdown `` blocks."""
code_block_re = re.compile(r'''
(?:\n\n|\A\n?)
( # $1 = the code block -- one or more lines, starting with a space/tab
(?:
(?:[ ]{%d} | \t) # Lines must start with a tab or a tab-width of spaces
.*\n+
)+
)
((?=^[ ]{0,%d}\S)|\Z) # Lookahead for non-space at line-start, or end of doc
''' % (self.tab_width, self.tab_width),
re.M | re.X)
return code_block_re.sub(self._code_block_sub, text)
_fenced_code_block_re = re.compile(r'''
(?:\n\n|\A\n?)
^```([\w+-]+)?[ \t]*\n # opening fence, $1 = optional lang
(.*?) # $2 = code block content
^```[ \t]*\n # closing fence
''', re.M | re.X | re.S)
def _fenced_code_block_sub(self, match):
return self._code_block_sub(match, is_fenced_code_block=True);
def _do_fenced_code_blocks(self, text):
"""Process ```-fenced unindented code blocks ('fenced-code-blocks' extra)."""
return self._fenced_code_block_re.sub(self._fenced_code_block_sub, text)
# Rules for a code span:
# - backslash escapes are not interpreted in a code span
# - to include one or or a run of more backticks the delimiters must
# be a longer run of backticks
# - cannot start or end a code span with a backtick; pad with a
# space and that space will be removed in the emitted HTML
# See `test/tm-cases/escapes.text` for a number of edge-case
# examples.
_code_span_re = re.compile(r'''
(?%s
" % c
def _do_code_spans(self, text):
# * Backtick quotes are used for
spans.
#
# * You can use multiple backticks as the delimiters if you want to
# include literal backticks in the code span. So, this input:
#
# Just type ``foo `bar` baz`` at the prompt.
#
# Will translate to:
#
# Just type foo `bar` baz
at the prompt.
#
# There's no arbitrary limit to the number of backticks you
# can use as delimters. If you need three consecutive backticks
# in your code, use four for delimiters, etc.
#
# * You can use spaces to get literal backticks at the edges:
#
# ... type `` `bar` `` ...
#
# Turns to:
#
# ... type `bar`
...
return self._code_span_re.sub(self._code_span_sub, text)
def _encode_code(self, text):
"""Encode/escape certain characters inside Markdown code runs.
The point is that in code, these characters are literals,
and lose their special Markdown meanings.
"""
replacements = [
# Encode all ampersands; HTML entities are not
# entities within a Markdown code span.
('&', '&'),
# Do the angle bracket song and dance:
('<', '<'),
('>', '>'),
]
for before, after in replacements:
text = text.replace(before, after)
hashed = _hash_text(text)
self._escape_table[text] = hashed
return hashed
_strong_re = re.compile(r"(\*\*|__)(?=\S)(.+?[*_]*)(?<=\S)\1", re.S)
_em_re = re.compile(r"(\*|_)(?=\S)(.+?)(?<=\S)\1", re.S)
_code_friendly_strong_re = re.compile(r"\*\*(?=\S)(.+?[*_]*)(?<=\S)\*\*", re.S)
_code_friendly_em_re = re.compile(r"\*(?=\S)(.+?)(?<=\S)\*", re.S)
def _do_italics_and_bold(self, text):
# must go first:
if "code-friendly" in self.extras:
text = self._code_friendly_strong_re.sub(r"\1", text)
text = self._code_friendly_em_re.sub(r"\1", text)
else:
text = self._strong_re.sub(r"\2", text)
text = self._em_re.sub(r"\2", text)
return text
# "smarty-pants" extra: Very liberal in interpreting a single prime as an
# apostrophe; e.g. ignores the fact that "round", "bout", "twer", and
# "twixt" can be written without an initial apostrophe. This is fine because
# using scare quotes (single quotation marks) is rare.
_apostrophe_year_re = re.compile(r"'(\d\d)(?=(\s|,|;|\.|\?|!|$))")
_contractions = ["tis", "twas", "twer", "neath", "o", "n",
"round", "bout", "twixt", "nuff", "fraid", "sup"]
def _do_smart_contractions(self, text):
text = self._apostrophe_year_re.sub(r"’\1", text)
for c in self._contractions:
text = text.replace("'%s" % c, "’%s" % c)
text = text.replace("'%s" % c.capitalize(),
"’%s" % c.capitalize())
return text
# Substitute double-quotes before single-quotes.
_opening_single_quote_re = re.compile(r"(?
See "test/tm-cases/smarty_pants.text" for a full discussion of the
support here and
for a
discussion of some diversion from the original SmartyPants.
"""
if "'" in text: # guard for perf
text = self._do_smart_contractions(text)
text = self._opening_single_quote_re.sub("‘", text)
text = self._closing_single_quote_re.sub("’", text)
if '"' in text: # guard for perf
text = self._opening_double_quote_re.sub("“", text)
text = self._closing_double_quote_re.sub("”", text)
text = text.replace("---", "—")
text = text.replace("--", "–")
text = text.replace("...", "…")
text = text.replace(" . . . ", "…")
text = text.replace(". . .", "…")
return text
_block_quote_re = re.compile(r'''
( # Wrap whole match in \1
(
^[ \t]*>[ \t]? # '>' at the start of a line
.+\n # rest of the first line
(.+\n)* # subsequent consecutive lines
\n* # blanks
)+
)
''', re.M | re.X)
_bq_one_level_re = re.compile('^[ \t]*>[ \t]?', re.M);
_html_pre_block_re = re.compile(r'(\s*.+?
)', re.S)
def _dedent_two_spaces_sub(self, match):
return re.sub(r'(?m)^ ', '', match.group(1))
def _block_quote_sub(self, match):
bq = match.group(1)
bq = self._bq_one_level_re.sub('', bq) # trim one level of quoting
bq = self._ws_only_line_re.sub('', bq) # trim whitespace-only lines
bq = self._run_block_gamut(bq) # recurse
bq = re.sub('(?m)^', ' ', bq)
# These leading spaces screw with content, so we need to fix that:
bq = self._html_pre_block_re.sub(self._dedent_two_spaces_sub, bq)
return "\n%s\n
\n\n" % bq
def _do_block_quotes(self, text):
if '>' not in text:
return text
return self._block_quote_re.sub(self._block_quote_sub, text)
def _form_paragraphs(self, text):
# Strip leading and trailing lines:
text = text.strip('\n')
# Wrap tags.
grafs = []
for i, graf in enumerate(re.split(r"\n{2,}", text)):
if graf in self.html_blocks:
# Unhashify HTML blocks
grafs.append(self.html_blocks[graf])
else:
cuddled_list = None
if "cuddled-lists" in self.extras:
# Need to put back trailing '\n' for `_list_item_re`
# match at the end of the paragraph.
li = self._list_item_re.search(graf + '\n')
# Two of the same list marker in this paragraph: a likely
# candidate for a list cuddled to preceding paragraph
# text (issue 33). Note the `[-1]` is a quick way to
# consider numeric bullets (e.g. "1." and "2.") to be
# equal.
if (li and len(li.group(2)) <= 3 and li.group("next_marker")
and li.group("marker")[-1] == li.group("next_marker")[-1]):
start = li.start()
cuddled_list = self._do_lists(graf[start:]).rstrip("\n")
assert cuddled_list.startswith("
") or cuddled_list.startswith("")
graf = graf[:start]
# Wrap tags.
graf = self._run_span_gamut(graf)
grafs.append("
" + graf.lstrip(" \t") + "
")
if cuddled_list:
grafs.append(cuddled_list)
return "\n\n".join(grafs)
def _add_footnotes(self, text):
if self.footnotes:
footer = [
'',
'
',
]
for i, id in enumerate(self.footnote_ids):
if i != 0:
footer.append('')
footer.append('- ' % id)
footer.append(self._run_block_gamut(self.footnotes[id]))
backlink = (''
'↩' % (id, i+1))
if footer[-1].endswith(""):
footer[-1] = footer[-1][:-len("")] \
+ ' ' + backlink + ""
else:
footer.append("\n
%s
" % backlink)
footer.append(' ')
footer.append('')
footer.append('')
return text + '\n\n' + '\n'.join(footer)
else:
return text
# Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin:
# http://bumppo.net/projects/amputator/
_ampersand_re = re.compile(r'&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)')
_naked_lt_re = re.compile(r'<(?![a-z/?\$!])', re.I)
_naked_gt_re = re.compile(r'''(?''', re.I)
def _encode_amps_and_angles(self, text):
# Smart processing for ampersands and angle brackets that need
# to be encoded.
text = self._ampersand_re.sub('&', text)
# Encode naked <'s
text = self._naked_lt_re.sub('<', text)
# Encode naked >'s
# Note: Other markdown implementations (e.g. Markdown.pl, PHP
# Markdown) don't do this.
text = self._naked_gt_re.sub('>', text)
return text
def _encode_backslash_escapes(self, text):
for ch, escape in list(self._escape_table.items()):
text = text.replace("\\"+ch, escape)
return text
_auto_link_re = re.compile(r'<((https?|ftp):[^\'">\s]+)>', re.I)
def _auto_link_sub(self, match):
g1 = match.group(1)
return '%s' % (g1, g1)
_auto_email_link_re = re.compile(r"""
<
(?:mailto:)?
(
[-.\w]+
\@
[-\w]+(\.[-\w]+)*\.[a-z]+
)
>
""", re.I | re.X | re.U)
def _auto_email_link_sub(self, match):
return self._encode_email_address(
self._unescape_special_chars(match.group(1)))
def _do_auto_links(self, text):
text = self._auto_link_re.sub(self._auto_link_sub, text)
text = self._auto_email_link_re.sub(self._auto_email_link_sub, text)
return text
def _encode_email_address(self, addr):
# Input: an email address, e.g. "foo@example.com"
#
# Output: the email address as a mailto link, with each character
# of the address encoded as either a decimal or hex entity, in
# the hopes of foiling most address harvesting spam bots. E.g.:
#
# foo
# @example.com
#
# Based on a filter by Matthew Wickline, posted to the BBEdit-Talk
# mailing list:
chars = [_xml_encode_email_char_at_random(ch)
for ch in "mailto:" + addr]
# Strip the mailto: from the visible part.
addr = '%s' \
% (''.join(chars), ''.join(chars[7:]))
return addr
def _do_link_patterns(self, text):
"""Caveat emptor: there isn't much guarding against link
patterns being formed inside other standard Markdown links, e.g.
inside a [link def][like this].
Dev Notes: *Could* consider prefixing regexes with a negative
lookbehind assertion to attempt to guard against this.
"""
link_from_hash = {}
for regex, repl in self.link_patterns:
replacements = []
for match in regex.finditer(text):
if hasattr(repl, "__call__"):
href = repl(match)
else:
href = match.expand(repl)
replacements.append((match.span(), href))
for (start, end), href in reversed(replacements):
escaped_href = (
href.replace('"', '"') # b/c of attr quote
# To avoid markdown and :
.replace('*', self._escape_table['*'])
.replace('_', self._escape_table['_']))
link = '%s' % (escaped_href, text[start:end])
hash = _hash_text(link)
link_from_hash[hash] = link
text = text[:start] + hash + text[end:]
for hash, link in list(link_from_hash.items()):
text = text.replace(hash, link)
return text
def _unescape_special_chars(self, text):
# Swap back in all the special characters we've hidden.
for ch, hash in list(self._escape_table.items()):
text = text.replace(hash, ch)
return text
def _outdent(self, text):
# Remove one level of line-leading tabs or spaces
return self._outdent_re.sub('', text)
class MarkdownWithExtras(Markdown):
"""A markdowner class that enables most extras:
- footnotes
- code-color (only has effect if 'pygments' Python module on path)
These are not included:
- pyshell (specific to Python-related documenting)
- code-friendly (because it *disables* part of the syntax)
- link-patterns (because you need to specify some actual
link-patterns anyway)
"""
extras = ["footnotes", "code-color"]
#---- internal support functions
class UnicodeWithAttrs(unicode):
"""A subclass of unicode used for the return value of conversion to
possibly attach some attributes. E.g. the "toc_html" attribute when
the "toc" extra is used.
"""
metadata = None
_toc = None
def toc_html(self):
"""Return the HTML for the current TOC.
This expects the `_toc` attribute to have been set on this instance.
"""
if self._toc is None:
return None
def indent():
return ' ' * (len(h_stack) - 1)
lines = []
h_stack = [0] # stack of header-level numbers
for level, id, name in self._toc:
if level > h_stack[-1]:
lines.append("%s" % indent())
h_stack.append(level)
elif level == h_stack[-1]:
lines[-1] += ""
else:
while level < h_stack[-1]:
h_stack.pop()
if not lines[-1].endswith(""):
lines[-1] += ""
lines.append("%s
" % indent())
lines.append('%s- %s' % (
indent(), id, name))
while len(h_stack) > 1:
h_stack.pop()
if not lines[-1].endswith("
"):
lines[-1] += ""
lines.append("%s
" % indent())
return '\n'.join(lines) + '\n'
toc_html = property(toc_html)
## {{{ http://code.activestate.com/recipes/577257/ (r1)
import re
char_map = {u'À': 'A', u'Á': 'A', u'Â': 'A', u'Ã': 'A', u'Ä': 'Ae', u'Å': 'A', u'Æ': 'A', u'Ā': 'A', u'Ą': 'A', u'Ă': 'A', u'Ç': 'C', u'Ć': 'C', u'Č': 'C', u'Ĉ': 'C', u'Ċ': 'C', u'Ď': 'D', u'Đ': 'D', u'È': 'E', u'É': 'E', u'Ê': 'E', u'Ë': 'E', u'Ē': 'E', u'Ę': 'E', u'Ě': 'E', u'Ĕ': 'E', u'Ė': 'E', u'Ĝ': 'G', u'Ğ': 'G', u'Ġ': 'G', u'Ģ': 'G', u'Ĥ': 'H', u'Ħ': 'H', u'Ì': 'I', u'Í': 'I', u'Î': 'I', u'Ï': 'I', u'Ī': 'I', u'Ĩ': 'I', u'Ĭ': 'I', u'Į': 'I', u'İ': 'I', u'IJ': 'IJ', u'Ĵ': 'J', u'Ķ': 'K', u'Ľ': 'K', u'Ĺ': 'K', u'Ļ': 'K', u'Ŀ': 'K', u'Ł': 'L', u'Ñ': 'N', u'Ń': 'N', u'Ň': 'N', u'Ņ': 'N', u'Ŋ': 'N', u'Ò': 'O', u'Ó': 'O', u'Ô': 'O', u'Õ': 'O', u'Ö': 'Oe', u'Ø': 'O', u'Ō': 'O', u'Ő': 'O', u'Ŏ': 'O', u'Œ': 'OE', u'Ŕ': 'R', u'Ř': 'R', u'Ŗ': 'R', u'Ś': 'S', u'Ş': 'S', u'Ŝ': 'S', u'Ș': 'S', u'Š': 'S', u'Ť': 'T', u'Ţ': 'T', u'Ŧ': 'T', u'Ț': 'T', u'Ù': 'U', u'Ú': 'U', u'Û': 'U', u'Ü': 'Ue', u'Ū': 'U', u'Ů': 'U', u'Ű': 'U', u'Ŭ': 'U', u'Ũ': 'U', u'Ų': 'U', u'Ŵ': 'W', u'Ŷ': 'Y', u'Ÿ': 'Y', u'Ý': 'Y', u'Ź': 'Z', u'Ż': 'Z', u'Ž': 'Z', u'à': 'a', u'á': 'a', u'â': 'a', u'ã': 'a', u'ä': 'ae', u'ā': 'a', u'ą': 'a', u'ă': 'a', u'å': 'a', u'æ': 'ae', u'ç': 'c', u'ć': 'c', u'č': 'c', u'ĉ': 'c', u'ċ': 'c', u'ď': 'd', u'đ': 'd', u'è': 'e', u'é': 'e', u'ê': 'e', u'ë': 'e', u'ē': 'e', u'ę': 'e', u'ě': 'e', u'ĕ': 'e', u'ė': 'e', u'ƒ': 'f', u'ĝ': 'g', u'ğ': 'g', u'ġ': 'g', u'ģ': 'g', u'ĥ': 'h', u'ħ': 'h', u'ì': 'i', u'í': 'i', u'î': 'i', u'ï': 'i', u'ī': 'i', u'ĩ': 'i', u'ĭ': 'i', u'į': 'i', u'ı': 'i', u'ij': 'ij', u'ĵ': 'j', u'ķ': 'k', u'ĸ': 'k', u'ł': 'l', u'ľ': 'l', u'ĺ': 'l', u'ļ': 'l', u'ŀ': 'l', u'ñ': 'n', u'ń': 'n', u'ň': 'n', u'ņ': 'n', u'ʼn': 'n', u'ŋ': 'n', u'ò': 'o', u'ó': 'o', u'ô': 'o', u'õ': 'o', u'ö': 'oe', u'ø': 'o', u'ō': 'o', u'ő': 'o', u'ŏ': 'o', u'œ': 'oe', u'ŕ': 'r', u'ř': 'r', u'ŗ': 'r', u'ś': 's', u'š': 's', u'ť': 't', u'ù': 'u', u'ú': 'u', u'û': 'u', u'ü': 'ue', u'ū': 'u', u'ů': 'u', u'ű': 'u', u'ŭ': 'u', u'ũ': 'u', u'ų': 'u', u'ŵ': 'w', u'ÿ': 'y', u'ý': 'y', u'ŷ': 'y', u'ż': 'z', u'ź': 'z', u'ž': 'z', u'ß': 'ss', u'ſ': 'ss', u'Α': 'A', u'Ά': 'A', u'Ἀ': 'A', u'Ἁ': 'A', u'Ἂ': 'A', u'Ἃ': 'A', u'Ἄ': 'A', u'Ἅ': 'A', u'Ἆ': 'A', u'Ἇ': 'A', u'ᾈ': 'A', u'ᾉ': 'A', u'ᾊ': 'A', u'ᾋ': 'A', u'ᾌ': 'A', u'ᾍ': 'A', u'ᾎ': 'A', u'ᾏ': 'A', u'Ᾰ': 'A', u'Ᾱ': 'A', u'Ὰ': 'A', u'Ά': 'A', u'ᾼ': 'A', u'Β': 'B', u'Γ': 'G', u'Δ': 'D', u'Ε': 'E', u'Έ': 'E', u'Ἐ': 'E', u'Ἑ': 'E', u'Ἒ': 'E', u'Ἓ': 'E', u'Ἔ': 'E', u'Ἕ': 'E', u'Έ': 'E', u'Ὲ': 'E', u'Ζ': 'Z', u'Η': 'I', u'Ή': 'I', u'Ἠ': 'I', u'Ἡ': 'I', u'Ἢ': 'I', u'Ἣ': 'I', u'Ἤ': 'I', u'Ἥ': 'I', u'Ἦ': 'I', u'Ἧ': 'I', u'ᾘ': 'I', u'ᾙ': 'I', u'ᾚ': 'I', u'ᾛ': 'I', u'ᾜ': 'I', u'ᾝ': 'I', u'ᾞ': 'I', u'ᾟ': 'I', u'Ὴ': 'I', u'Ή': 'I', u'ῌ': 'I', u'Θ': 'TH', u'Ι': 'I', u'Ί': 'I', u'Ϊ': 'I', u'Ἰ': 'I', u'Ἱ': 'I', u'Ἲ': 'I', u'Ἳ': 'I', u'Ἴ': 'I', u'Ἵ': 'I', u'Ἶ': 'I', u'Ἷ': 'I', u'Ῐ': 'I', u'Ῑ': 'I', u'Ὶ': 'I', u'Ί': 'I', u'Κ': 'K', u'Λ': 'L', u'Μ': 'M', u'Ν': 'N', u'Ξ': 'KS', u'Ο': 'O', u'Ό': 'O', u'Ὀ': 'O', u'Ὁ': 'O', u'Ὂ': 'O', u'Ὃ': 'O', u'Ὄ': 'O', u'Ὅ': 'O', u'Ὸ': 'O', u'Ό': 'O', u'Π': 'P', u'Ρ': 'R', u'Ῥ': 'R', u'Σ': 'S', u'Τ': 'T', u'Υ': 'Y', u'Ύ': 'Y', u'Ϋ': 'Y', u'Ὑ': 'Y', u'Ὓ': 'Y', u'Ὕ': 'Y', u'Ὗ': 'Y', u'Ῠ': 'Y', u'Ῡ': 'Y', u'Ὺ': 'Y', u'Ύ': 'Y', u'Φ': 'F', u'Χ': 'X', u'Ψ': 'PS', u'Ω': 'O', u'Ώ': 'O', u'Ὠ': 'O', u'Ὡ': 'O', u'Ὢ': 'O', u'Ὣ': 'O', u'Ὤ': 'O', u'Ὥ': 'O', u'Ὦ': 'O', u'Ὧ': 'O', u'ᾨ': 'O', u'ᾩ': 'O', u'ᾪ': 'O', u'ᾫ': 'O', u'ᾬ': 'O', u'ᾭ': 'O', u'ᾮ': 'O', u'ᾯ': 'O', u'Ὼ': 'O', u'Ώ': 'O', u'ῼ': 'O', u'α': 'a', u'ά': 'a', u'ἀ': 'a', u'ἁ': 'a', u'ἂ': 'a', u'ἃ': 'a', u'ἄ': 'a', u'ἅ': 'a', u'ἆ': 'a', u'ἇ': 'a', u'ᾀ': 'a', u'ᾁ': 'a', u'ᾂ': 'a', u'ᾃ': 'a', u'ᾄ': 'a', u'ᾅ': 'a', u'ᾆ': 'a', u'ᾇ': 'a', u'ὰ': 'a', u'ά': 'a', u'ᾰ': 'a', u'ᾱ': 'a', u'ᾲ': 'a', u'ᾳ': 'a', u'ᾴ': 'a', u'ᾶ': 'a', u'ᾷ': 'a', u'β': 'b', u'γ': 'g', u'δ': 'd', u'ε': 'e', u'έ': 'e', u'ἐ': 'e', u'ἑ': 'e', u'ἒ': 'e', u'ἓ': 'e', u'ἔ': 'e', u'ἕ': 'e', u'ὲ': 'e', u'έ': 'e', u'ζ': 'z', u'η': 'i', u'ή': 'i', u'ἠ': 'i', u'ἡ': 'i', u'ἢ': 'i', u'ἣ': 'i', u'ἤ': 'i', u'ἥ': 'i', u'ἦ': 'i', u'ἧ': 'i', u'ᾐ': 'i', u'ᾑ': 'i', u'ᾒ': 'i', u'ᾓ': 'i', u'ᾔ': 'i', u'ᾕ': 'i', u'ᾖ': 'i', u'ᾗ': 'i', u'ὴ': 'i', u'ή': 'i', u'ῂ': 'i', u'ῃ': 'i', u'ῄ': 'i', u'ῆ': 'i', u'ῇ': 'i', u'θ': 'th', u'ι': 'i', u'ί': 'i', u'ϊ': 'i', u'ΐ': 'i', u'ἰ': 'i', u'ἱ': 'i', u'ἲ': 'i', u'ἳ': 'i', u'ἴ': 'i', u'ἵ': 'i', u'ἶ': 'i', u'ἷ': 'i', u'ὶ': 'i', u'ί': 'i', u'ῐ': 'i', u'ῑ': 'i', u'ῒ': 'i', u'ΐ': 'i', u'ῖ': 'i', u'ῗ': 'i', u'κ': 'k', u'λ': 'l', u'μ': 'm', u'ν': 'n', u'ξ': 'ks', u'ο': 'o', u'ό': 'o', u'ὀ': 'o', u'ὁ': 'o', u'ὂ': 'o', u'ὃ': 'o', u'ὄ': 'o', u'ὅ': 'o', u'ὸ': 'o', u'ό': 'o', u'π': 'p', u'ρ': 'r', u'ῤ': 'r', u'ῥ': 'r', u'σ': 's', u'ς': 's', u'τ': 't', u'υ': 'y', u'ύ': 'y', u'ϋ': 'y', u'ΰ': 'y', u'ὐ': 'y', u'ὑ': 'y', u'ὒ': 'y', u'ὓ': 'y', u'ὔ': 'y', u'ὕ': 'y', u'ὖ': 'y', u'ὗ': 'y', u'ὺ': 'y', u'ύ': 'y', u'ῠ': 'y', u'ῡ': 'y', u'ῢ': 'y', u'ΰ': 'y', u'ῦ': 'y', u'ῧ': 'y', u'φ': 'f', u'χ': 'x', u'ψ': 'ps', u'ω': 'o', u'ώ': 'o', u'ὠ': 'o', u'ὡ': 'o', u'ὢ': 'o', u'ὣ': 'o', u'ὤ': 'o', u'ὥ': 'o', u'ὦ': 'o', u'ὧ': 'o', u'ᾠ': 'o', u'ᾡ': 'o', u'ᾢ': 'o', u'ᾣ': 'o', u'ᾤ': 'o', u'ᾥ': 'o', u'ᾦ': 'o', u'ᾧ': 'o', u'ὼ': 'o', u'ώ': 'o', u'ῲ': 'o', u'ῳ': 'o', u'ῴ': 'o', u'ῶ': 'o', u'ῷ': 'o', u'¨': '', u'΅': '', u'᾿': '', u'῾': '', u'῍': '', u'῝': '', u'῎': '', u'῞': '', u'῏': '', u'῟': '', u'῀': '', u'῁': '', u'΄': '', u'΅': '', u'`': '', u'῭': '', u'ͺ': '', u'᾽': '', u'А': 'A', u'Б': 'B', u'В': 'V', u'Г': 'G', u'Д': 'D', u'Е': 'E', u'Ё': 'YO', u'Ж': 'ZH', u'З': 'Z', u'И': 'I', u'Й': 'J', u'К': 'K', u'Л': 'L', u'М': 'M', u'Н': 'N', u'О': 'O', u'П': 'P', u'Р': 'R', u'С': 'S', u'Т': 'T', u'У': 'U', u'Ф': 'F', u'Х': 'H', u'Ц': 'TS', u'Ч': 'CH', u'Ш': 'SH', u'Щ': 'SCH', u'Ы': 'YI', u'Э': 'E', u'Ю': 'YU', u'Я': 'YA', u'а': 'A', u'б': 'B', u'в': 'V', u'г': 'G', u'д': 'D', u'е': 'E', u'ё': 'YO', u'ж': 'ZH', u'з': 'Z', u'и': 'I', u'й': 'J', u'к': 'K', u'л': 'L', u'м': 'M', u'н': 'N', u'о': 'O', u'п': 'P', u'р': 'R', u'с': 'S', u'т': 'T', u'у': 'U', u'ф': 'F', u'х': 'H', u'ц': 'TS', u'ч': 'CH', u'ш': 'SH', u'щ': 'SCH', u'ы': 'YI', u'э': 'E', u'ю': 'YU', u'я': 'YA', u'Ъ': '', u'ъ': '', u'Ь': '', u'ь': '', u'ð': 'd', u'Ð': 'D', u'þ': 'th', u'Þ': 'TH',u'ა': 'a', u'ბ': 'b', u'გ': 'g', u'დ': 'd', u'ე': 'e', u'ვ': 'v', u'ზ': 'z', u'თ': 't', u'ი': 'i', u'კ': 'k', u'ლ': 'l', u'მ': 'm', u'ნ': 'n', u'ო': 'o', u'პ': 'p', u'ჟ': 'zh', u'რ': 'r', u'ს': 's', u'ტ': 't', u'უ': 'u', u'ფ': 'p', u'ქ': 'k', u'ღ': 'gh', u'ყ': 'q', u'შ': 'sh', u'ჩ': 'ch', u'ც': 'ts', u'ძ': 'dz', u'წ': 'ts', u'ჭ': 'ch', u'ხ': 'kh', u'ჯ': 'j', u'ჰ': 'h' }
def replace_char(m):
char = m.group()
if char_map.has_key(char):
return char_map[char]
else:
return char
_punct_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+')
def _slugify(text, delim=u'-'):
"""Generates an ASCII-only slug."""
result = []
for word in _punct_re.split(text.lower()):
word = word.encode('utf-8')
if word:
result.append(word)
slugified = delim.join([i.decode('utf-8') for i in result])
return re.sub('[^a-zA-Z0-9\\s\\-]{1}', replace_char, slugified).lower()
## end of http://code.activestate.com/recipes/577257/ }}}
# From http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52549
def _curry(*args, **kwargs):
function, args = args[0], args[1:]
def result(*rest, **kwrest):
combined = kwargs.copy()
combined.update(kwrest)
return function(*args + rest, **combined)
return result
# Recipe: regex_from_encoded_pattern (1.0)
def _regex_from_encoded_pattern(s):
"""'foo' -> re.compile(re.escape('foo'))
'/foo/' -> re.compile('foo')
'/foo/i' -> re.compile('foo', re.I)
"""
if s.startswith('/') and s.rfind('/') != 0:
# Parse it: /PATTERN/FLAGS
idx = s.rfind('/')
pattern, flags_str = s[1:idx], s[idx+1:]
flag_from_char = {
"i": re.IGNORECASE,
"l": re.LOCALE,
"s": re.DOTALL,
"m": re.MULTILINE,
"u": re.UNICODE,
}
flags = 0
for char in flags_str:
try:
flags |= flag_from_char[char]
except KeyError:
raise ValueError("unsupported regex flag: '%s' in '%s' "
"(must be one of '%s')"
% (char, s, ''.join(list(flag_from_char.keys()))))
return re.compile(s[1:idx], flags)
else: # not an encoded regex
return re.compile(re.escape(s))
# Recipe: dedent (0.1.2)
def _dedentlines(lines, tabsize=8, skip_first_line=False):
"""_dedentlines(lines, tabsize=8, skip_first_line=False) -> dedented lines
"lines" is a list of lines to dedent.
"tabsize" is the tab width to use for indent width calculations.
"skip_first_line" is a boolean indicating if the first line should
be skipped for calculating the indent width and for dedenting.
This is sometimes useful for docstrings and similar.
Same as dedent() except operates on a sequence of lines. Note: the
lines list is modified **in-place**.
"""
DEBUG = False
if DEBUG:
print("dedent: dedent(..., tabsize=%d, skip_first_line=%r)"\
% (tabsize, skip_first_line))
indents = []
margin = None
for i, line in enumerate(lines):
if i == 0 and skip_first_line: continue
indent = 0
for ch in line:
if ch == ' ':
indent += 1
elif ch == '\t':
indent += tabsize - (indent % tabsize)
elif ch in '\r\n':
continue # skip all-whitespace lines
else:
break
else:
continue # skip all-whitespace lines
if DEBUG: print("dedent: indent=%d: %r" % (indent, line))
if margin is None:
margin = indent
else:
margin = min(margin, indent)
if DEBUG: print("dedent: margin=%r" % margin)
if margin is not None and margin > 0:
for i, line in enumerate(lines):
if i == 0 and skip_first_line: continue
removed = 0
for j, ch in enumerate(line):
if ch == ' ':
removed += 1
elif ch == '\t':
removed += tabsize - (removed % tabsize)
elif ch in '\r\n':
if DEBUG: print("dedent: %r: EOL -> strip up to EOL" % line)
lines[i] = lines[i][j:]
break
else:
raise ValueError("unexpected non-whitespace char %r in "
"line %r while removing %d-space margin"
% (ch, line, margin))
if DEBUG:
print("dedent: %r: %r -> removed %d/%d"\
% (line, ch, removed, margin))
if removed == margin:
lines[i] = lines[i][j+1:]
break
elif removed > margin:
lines[i] = ' '*(removed-margin) + lines[i][j+1:]
break
else:
if removed:
lines[i] = lines[i][removed:]
return lines
def _dedent(text, tabsize=8, skip_first_line=False):
"""_dedent(text, tabsize=8, skip_first_line=False) -> dedented text
"text" is the text to dedent.
"tabsize" is the tab width to use for indent width calculations.
"skip_first_line" is a boolean indicating if the first line should
be skipped for calculating the indent width and for dedenting.
This is sometimes useful for docstrings and similar.
textwrap.dedent(s), but don't expand tabs to spaces
"""
lines = text.splitlines(1)
_dedentlines(lines, tabsize=tabsize, skip_first_line=skip_first_line)
return ''.join(lines)
class _memoized:
"""Decorator that caches a function's return value each time it is called.
If called later with the same arguments, the cached value is returned, and
not re-evaluated.
http://wiki.python.org/moin/PythonDecoratorLibrary
"""
def __init__(self, func):
self.func = func
self.cache = {}
def __call__(self, *args):
try:
return self.cache[args]
except KeyError:
self.cache[args] = value = self.func(*args)
return value
except TypeError:
# uncachable -- for instance, passing a list as an argument.
# Better to not cache than to blow up entirely.
return self.func(*args)
def __repr__(self):
"""Return the function's docstring."""
return self.func.__doc__
def _xml_oneliner_re_from_tab_width(tab_width):
"""Standalone XML processing instruction regex."""
return re.compile(r"""
(?:
(?<=\n\n) # Starting after a blank line
| # or
\A\n? # the beginning of the doc
)
( # save in $1
[ ]{0,%d}
(?:
<\?\w+\b\s+.*?\?> # XML processing instruction
|
<\w+:\w+\b\s+.*?/> # namespaced single tag
)
[ \t]*
(?=\n{2,}|\Z) # followed by a blank line or end of document
)
""" % (tab_width - 1), re.X)
_xml_oneliner_re_from_tab_width = _memoized(_xml_oneliner_re_from_tab_width)
def _hr_tag_re_from_tab_width(tab_width):
return re.compile(r"""
(?:
(?<=\n\n) # Starting after a blank line
| # or
\A\n? # the beginning of the doc
)
( # save in \1
[ ]{0,%d}
<(hr) # start tag = \2
\b # word break
([^<>])*? #
/?> # the matching end tag
[ \t]*
(?=\n{2,}|\Z) # followed by a blank line or end of document
)
""" % (tab_width - 1), re.X)
_hr_tag_re_from_tab_width = _memoized(_hr_tag_re_from_tab_width)
def _xml_escape_attr(attr, skip_single_quote=True):
"""Escape the given string for use in an HTML/XML tag attribute.
By default this doesn't bother with escaping `'` to `'`, presuming that
the tag attribute is surrounded by double quotes.
"""
escaped = (attr
.replace('&', '&')
.replace('"', '"')
.replace('<', '<')
.replace('>', '>'))
if not skip_single_quote:
escaped = escaped.replace("'", "'")
return escaped
def _xml_encode_email_char_at_random(ch):
r = random()
# Roughly 10% raw, 45% hex, 45% dec.
# '@' *must* be encoded. I [John Gruber] insist.
# Issue 26: '_' must be encoded.
if r > 0.9 and ch not in "@_":
return ch
elif r < 0.45:
# The [1:] is to drop leading '0': 0x63 -> x63
return '%s;' % hex(ord(ch))[1:]
else:
return '%s;' % ord(ch)
#---- mainline
class _NoReflowFormatter(optparse.IndentedHelpFormatter):
"""An optparse formatter that does NOT reflow the description."""
def format_description(self, description):
return description or ""
def _test():
import doctest
doctest.testmod()
def main(argv=None):
if argv is None:
argv = sys.argv
if not logging.root.handlers:
logging.basicConfig()
usage = "usage: %prog [PATHS...]"
version = "%prog "+__version__
parser = optparse.OptionParser(prog="markdown2", usage=usage,
version=version, description=cmdln_desc,
formatter=_NoReflowFormatter())
parser.add_option("-v", "--verbose", dest="log_level",
action="store_const", const=logging.DEBUG,
help="more verbose output")
parser.add_option("--encoding",
help="specify encoding of text content")
parser.add_option("--html4tags", action="store_true", default=False,
help="use HTML 4 style for empty element tags")
parser.add_option("-s", "--safe", metavar="MODE", dest="safe_mode",
help="sanitize literal HTML: 'escape' escapes "
"HTML meta chars, 'replace' replaces with an "
"[HTML_REMOVED] note")
parser.add_option("-x", "--extras", action="append",
help="Turn on specific extra features (not part of "
"the core Markdown spec). See above.")
parser.add_option("--use-file-vars",
help="Look for and use Emacs-style 'markdown-extras' "
"file var to turn on extras. See "
"")
parser.add_option("--link-patterns-file",
help="path to a link pattern file")
parser.add_option("--self-test", action="store_true",
help="run internal self-tests (some doctests)")
parser.add_option("--compare", action="store_true",
help="run against Markdown.pl as well (for testing)")
parser.set_defaults(log_level=logging.INFO, compare=False,
encoding="utf-8", safe_mode=None, use_file_vars=False)
opts, paths = parser.parse_args()
log.setLevel(opts.log_level)
if opts.self_test:
return _test()
if opts.extras:
extras = {}
for s in opts.extras:
splitter = re.compile("[,;: ]+")
for e in splitter.split(s):
if '=' in e:
ename, earg = e.split('=', 1)
try:
earg = int(earg)
except ValueError:
pass
else:
ename, earg = e, None
extras[ename] = earg
else:
extras = None
if opts.link_patterns_file:
link_patterns = []
f = open(opts.link_patterns_file)
try:
for i, line in enumerate(f.readlines()):
if not line.strip(): continue
if line.lstrip().startswith("#"): continue
try:
pat, href = line.rstrip().rsplit(None, 1)
except ValueError:
raise MarkdownError("%s:%d: invalid link pattern line: %r"
% (opts.link_patterns_file, i+1, line))
link_patterns.append(
(_regex_from_encoded_pattern(pat), href))
finally:
f.close()
else:
link_patterns = None
from os.path import join, dirname, abspath, exists
markdown_pl = join(dirname(dirname(abspath(__file__))), "test",
"Markdown.pl")
if not paths:
paths = ['-']
for path in paths:
if path == '-':
text = sys.stdin.read()
else:
fp = codecs.open(path, 'r', opts.encoding)
text = fp.read()
fp.close()
if opts.compare:
from subprocess import Popen, PIPE
print("==== Markdown.pl ====")
p = Popen('perl %s' % markdown_pl, shell=True, stdin=PIPE, stdout=PIPE, close_fds=True)
p.stdin.write(text.encode('utf-8'))
p.stdin.close()
perl_html = p.stdout.read().decode('utf-8')
if py3:
sys.stdout.write(perl_html)
else:
sys.stdout.write(perl_html.encode(
sys.stdout.encoding or "utf-8", 'xmlcharrefreplace'))
print("==== markdown2.py ====")
html = markdown(text,
html4tags=opts.html4tags,
safe_mode=opts.safe_mode,
extras=extras, link_patterns=link_patterns,
use_file_vars=opts.use_file_vars)
if py3:
sys.stdout.write(html)
else:
sys.stdout.write(html.encode(
sys.stdout.encoding or "utf-8", 'xmlcharrefreplace'))
if extras and "toc" in extras:
log.debug("toc_html: " +
html.toc_html.encode(sys.stdout.encoding or "utf-8", 'xmlcharrefreplace'))
if opts.compare:
test_dir = join(dirname(dirname(abspath(__file__))), "test")
if exists(join(test_dir, "test_markdown2.py")):
sys.path.insert(0, test_dir)
from test_markdown2 import norm_html_from_html
norm_html = norm_html_from_html(html)
norm_perl_html = norm_html_from_html(perl_html)
else:
norm_html = html
norm_perl_html = perl_html
print("==== match? %r ====" % (norm_perl_html == norm_html))
template_html = u'''
'''
def force_text(text):
if isinstance(text, unicode):
return text
else:
return text.decode('utf-8')
class BackDoc:
def __init__(self, markdown_converter, template_html, stdin, stdout):
self.markdown_converter = markdown_converter
self.template_html = force_text(template_html)
self.stdin = stdin
self.stdout = stdout
self.parser = self.get_parser()
def run(self, argv):
kwargs = self.get_kwargs(argv)
self.stdout.write(self.get_result_html(**kwargs))
def get_kwargs(self, argv):
parsed = dict(self.parser.parse_args(argv)._get_kwargs())
return self.prepare_kwargs_from_parsed_data(parsed)
def prepare_kwargs_from_parsed_data(self, parsed):
kwargs = {}
kwargs['title'] = force_text(parsed.get('title') or 'Documentation')
if parsed.get('source'):
kwargs['markdown_src'] = open(parsed['source'], 'r').read()
else:
kwargs['markdown_src'] = self.stdin.read()
kwargs['markdown_src'] = force_text(kwargs['markdown_src'] or '')
return kwargs
def get_result_html(self, title, markdown_src):
response = self.get_converted_to_html_response(markdown_src)
return (
self.template_html.replace('', title)
.replace('', response.toc_html and force_text(response.toc_html) or '')
.replace('', force_text(response))
)
def get_converted_to_html_response(self, markdown_src):
return self.markdown_converter.convert(markdown_src)
def get_parser(self):
parser = argparse.ArgumentParser()
parser.add_argument(
'-t',
'--title',
help='Documentation title header',
required=False,
)
parser.add_argument(
'-s',
'--source',
help='Markdown source file path',
required=False,
)
return parser
if __name__ == '__main__':
BackDoc(
markdown_converter=Markdown(extras=['toc']),
template_html=template_html,
stdin=sys.stdin,
stdout=sys.stdout
).run(argv=sys.argv[1:])
drf-extensions-0.7.1/docs/index.html 0000664 0000000 0000000 00000000000 14100726230 0017375 0 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/docs/index.md 0000664 0000000 0000000 00000265570 14100726230 0017062 0 ustar 00root root 0000000 0000000 ### DRF-extensions
DRF-extensions is a collection of custom extensions for [Django REST Framework](https://github.com/tomchristie/django-rest-framework).
Source repository is available at [https://github.com/chibisov/drf-extensions](https://github.com/chibisov/drf-extensions).
### Viewsets
Extensions for [viewsets](http://django-rest-framework.org/api-guide/viewsets.html).
#### DetailSerializerMixin
This mixin lets add custom serializer for detail view. Just add mixin and specify `serializer_detail_class` attribute:
from django.contrib.auth.models import User
from myapps.serializers import UserSerializer, UserDetailSerializer
from rest_framework_extensions.mixins import DetailSerializerMixin
class UserViewSet(DetailSerializerMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = UserSerializer
serializer_detail_class = UserDetailSerializer
queryset = User.objects.all()
Sometimes you need to set custom QuerySet for detail view. For example, in detail view you want to show user groups and permissions for these groups. You can make it by specifying `queryset_detail` attribute:
from django.contrib.auth.models import User
from myapps.serializers import UserSerializer, UserDetailSerializer
from rest_framework_extensions.mixins import DetailSerializerMixin
class UserViewSet(DetailSerializerMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = UserSerializer
serializer_detail_class = UserDetailSerializer
queryset = User.objects.all()
queryset_detail = queryset.prefetch_related('groups__permissions')
If you use `DetailSerializerMixin` and don't specify `serializer_detail_class` attribute, then `serializer_class` will be used.
If you use `DetailSerializerMixin` and don't specify `queryset_detail` attribute, then `queryset` will be used.
#### PaginateByMaxMixin
*New in DRF-extensions 0.2.2*
This mixin allows to paginate results by [max\_paginate\_by](http://www.django-rest-framework.org/api-guide/pagination#pagination-in-the-generic-views)
value. This approach is useful when clients want to take as much paginated data as possible,
but don't want to bother about backend limitations.
from myapps.serializers import UserSerializer
from rest_framework_extensions.mixins import PaginateByMaxMixin
class UserViewSet(PaginateByMaxMixin,
viewsets.ReadOnlyModelViewSet):
max_paginate_by = 100
serializer_class = UserSerializer
And now you can send requests with `?page_size=max` argument:
# Request
GET /users/?page_size=max HTTP/1.1
Accept: application/json
# Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
{
count: 1000,
next: "https://localhost:8000/v1/users/?page=2&page_size=max",
previous: null,
results: [
...100 items...
]
}
This mixin could be used only with Django Rest Framework >= 2.3.8, because
[max\_paginate\_by](http://www.django-rest-framework.org/topics/release-notes#238)
was introduced in 2.3.8 version.
#### Cache/ETAG mixins
The etag functionality is pending an overhaul has been temporarily removed since 0.4.0.
ReadOnlyCacheResponseAndETAGMixin and CacheResponseAndETAGMixin are no longer available to use.
See discussion in [Issue #177](https://github.com/chibisov/drf-extensions/issues/177)
### Routers
Extensions for [routers](http://django-rest-framework.org/api-guide/routers.html).
You will need to use custom `ExtendedDefaultRouter` or `ExtendedSimpleRouter` for routing if you want to take advantages of described extensions. For example you have standard implementation:
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
You should replace `DefaultRouter` with `ExtendedDefaultRouter`:
from rest_framework_extensions.routers import (
ExtendedDefaultRouter as DefaultRouter
)
router = DefaultRouter()
Or `SimpleRouter` with `ExtendedSimpleRouter`:
from rest_framework_extensions.routers import (
ExtendedSimpleRouter as SimpleRouter
)
router = SimpleRouter()
#### Pluggable router mixins
*New in DRF-extensions 0.2.4*
Every feature in extended routers has it's own mixin. That means that you can use the only features you need in your custom
routers. `ExtendedRouterMixin` has all set of drf-extensions features. For example you can use it with third-party routes:
from rest_framework_extensions.routers import ExtendedRouterMixin
from third_party_app.routers import SomeRouter
class ExtendedSomeRouter(ExtendedRouterMixin, SomeRouter):
pass
### Nested routes
*New in DRF-extensions 0.2.4*
Nested routes allows you create nested resources with [viewsets](http://www.django-rest-framework.org/api-guide/viewsets.html).
For example:
from rest_framework_extensions.routers import ExtendedSimpleRouter
from yourapp.views import (
UserViewSet,
GroupViewSet,
PermissionViewSet,
)
router = ExtendedSimpleRouter()
(
router.register(r'users', UserViewSet, basename='user')
.register(r'groups',
GroupViewSet,
basename='users-group',
parents_query_lookups=['user_groups'])
.register(r'permissions',
PermissionViewSet,
basename='users-groups-permission',
parents_query_lookups=['group__user', 'group'])
)
urlpatterns = router.urls
There is one requirement for viewsets which used in nested routers. They should add mixin `NestedViewSetMixin`. That mixin
adds automatic filtering by parent lookups:
# yourapp.views
from rest_framework_extensions.mixins import NestedViewSetMixin
class UserViewSet(NestedViewSetMixin, ModelViewSet):
model = UserModel
class GroupViewSet(NestedViewSetMixin, ModelViewSet):
model = GroupModel
class PermissionViewSet(NestedViewSetMixin, ModelViewSet):
model = PermissionModel
With such kind of router we have next resources:
* `/users/` - list of all users. Resolve name is **user-list**
* `/users//` - user detail. Resolve name is **user-detail**
* `/users//groups/` - list of groups for exact user.
Resolve name is **users-group-list**
* `/users//groups//` - user group detail. If user doesn't have group then resource will
be not found. Resolve name is **users-group-detail**
* `/users//groups//permissions/` - list of permissions for user group.
Resolve name is **users-groups-permission-list**
* `/users//groups//permissions//` - user group permission detail.
If user doesn't have group or group doesn't have permission then resource will be not found.
Resolve name is **users-groups-permission-detail**
Every resource is automatically filtered by parent lookups.
# Request
GET /users/1/groups/2/permissions/ HTTP/1.1
Accept: application/json
# Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
[
{
id: 3,
name: "read"
},
{
id: 4,
name: "update"
},
{
id: 5,
name: "delete"
}
]
For request above permissions will be filtered by user with pk `1` and group with pk `2`:
Permission.objects.filter(group__user=1, group=2)
Example with registering more then one nested resource in one depth:
permissions_routes = router.register(
r'permissions',
PermissionViewSet,
basename='permission'
)
permissions_routes.register(
r'groups',
GroupViewSet,
basename='permissions-group',
parents_query_lookups=['permissions']
)
permissions_routes.register(
r'users',
UserViewSet,
basename='permissions-user',
parents_query_lookups=['groups__permissions']
)
With such kind of router we have next resources:
* `/permissions/` - list of all permissions. Resolve name is **permission-list**
* `/permissions//` - permission detail. Resolve name is **permission-detail**
* `/permissions//groups/` - list of groups for exact permission.
Resolve name is **permissions-group-list**
* `/permissions//groups//` - permission group detail. If group doesn't have
permission then resource will be not found. Resolve name is **permissions-group-detail**
* `/permissions//users/` - list of users for exact permission.
Resolve name is **permissions-user-list**
* `/permissions//user//` - permission user detail. If user doesn't have
permission then resource will be not found. Resolve name is **permissions-user-detail**
#### Nested router mixin
You can use `rest_framework_extensions.routers.NestedRouterMixin` for adding nesting feature into your routers:
from rest_framework_extensions.routers import NestedRouterMixin
from rest_framework.routers import SimpleRouter
class SimpleRouterWithNesting(NestedRouterMixin, SimpleRouter):
pass
#### Usage with generic relations
If you want to use nested router for [generic relation](https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#generic-relations)
fields, you should explicitly filter `QuerySet` by content type.
For example if you have such kind of models:
class Task(models.Model):
title = models.CharField(max_length=30)
class Book(models.Model):
title = models.CharField(max_length=30)
class Comment(models.Model):
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey()
text = models.CharField(max_length=30)
Lets create viewsets for that models:
class TaskViewSet(NestedViewSetMixin, ModelViewSet):
model = TaskModel
class BookViewSet(NestedViewSetMixin, ModelViewSet):
model = BookModel
class CommentViewSet(NestedViewSetMixin, ModelViewSet):
queryset = CommentModel.objects.all()
And router like this:
router = ExtendedSimpleRouter()
# tasks route
(
router.register(r'tasks', TaskViewSet)
.register(r'comments',
CommentViewSet,
'tasks-comment',
parents_query_lookups=['object_id'])
)
# books route
(
router.register(r'books', BookViewSet)
.register(r'comments',
CommentViewSet,
'books-comment',
parents_query_lookups=['object_id'])
)
As you can see we've added to `parents_query_lookups` only one `object_id` value. But when you make requests to `comments`
endpoint for both tasks and books routes there is no context for current content type.
# Request
GET /tasks/123/comments/ HTTP/1.1
Accept: application/json
# Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
[
{
id: 1,
content_type: 1,
object_id: 123,
text: "Good task!"
},
{
id: 2,
content_type: 2, // oops. Wrong content type (for book)
object_id: 123, // task and book has the same id
text: "Good book!"
},
]
For such kind of cases you should explicitly filter `QuerySets` of nested viewsets by content type:
from django.contrib.contenttypes.models import ContentType
class CommentViewSet(NestedViewSetMixin, ModelViewSet):
queryset = CommentModel.objects.all()
class TaskCommentViewSet(CommentViewSet):
def get_queryset(self):
return super(TaskCommentViewSet, self).get_queryset().filter(
content_type=ContentType.objects.get_for_model(TaskModel)
)
class BookCommentViewSet(CommentViewSet):
def get_queryset(self):
return super(BookCommentViewSet, self).get_queryset().filter(
content_type=ContentType.objects.get_for_model(BookModel)
)
Lets use new viewsets in router:
router = ExtendedSimpleRouter()
# tasks route
(
router.register(r'tasks', TaskViewSet)
.register(r'comments',
TaskCommentViewSet,
'tasks-comment',
parents_query_lookups=['object_id'])
)
# books route
(
router.register(r'books', BookViewSet)
.register(r'comments',
BookCommentViewSet,
'books-comment',
parents_query_lookups=['object_id'])
)
### Serializers
Extensions for [serializers](http://www.django-rest-framework.org/api-guide/serializers) functionality.
#### PartialUpdateSerializerMixin
*New in DRF-extensions 0.2.3*
By default every saving of [ModelSerializer](http://www.django-rest-framework.org/api-guide/serializers#modelserializer)
saves the whole object. Even partial update just patches model instance. For example:
from myapps.models import City
from myapps.serializers import CitySerializer
moscow = City.objects.get(pk=10)
city_serializer = CitySerializer(
instance=moscow,
data={'country': 'USA'},
partial=True
)
if city_serializer.is_valid():
city_serializer.save()
# equivalent to
moscow.country = 'USA'
moscow.save()
SQL representation for previous example will be:
UPDATE city SET name='Moscow', country='USA' WHERE id=1;
Django's `save` method has keyword argument [update_fields](https://docs.djangoproject.com/en/dev/ref/models/instances/#specifying-which-fields-to-save).
Only the fields named in that list will be updated:
moscow.country = 'USA'
moscow.save(update_fields=['country'])
SQL representation for example with `update_fields` usage will be:
UPDATE city SET country='USA' WHERE id=1;
To use `update_fields` for every partial update you should mixin `PartialUpdateSerializerMixin` to your serializer:
from rest_framework_extensions.serializers import (
PartialUpdateSerializerMixin
)
class CitySerializer(PartialUpdateSerializerMixin,
serializers.ModelSerializer):
class Meta:
model = City
### Fields
Set of serializer fields that extends [default fields](http://www.django-rest-framework.org/api-guide/fields) functionality.
#### ResourceUriField
Represents a hyperlinking uri that points to the detail view for that object.
from rest_framework_extensions.fields import ResourceUriField
class CitySerializer(serializers.ModelSerializer):
resource_uri = ResourceUriField(view_name='city-detail')
class Meta:
model = City
Request example:
# Request
GET /cities/268/ HTTP/1.1
Accept: application/json
# Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
{
id: 268,
resource_uri: "http://localhost:8000/v1/cities/268/",
name: "Serpuhov"
}
### Permissions
Extensions for [permissions](http://www.django-rest-framework.org/api-guide/permissions.html).
#### Object permissions
*New in DRF-extensions 0.2.2*
Django Rest Framework allows you to use [DjangoObjectPermissions](http://www.django-rest-framework.org/api-guide/permissions#djangoobjectpermissions) out of the box. But it has one limitation - if user has no permissions for viewing resource he will get `404` as response code. In most cases it's good approach because it solves security issues by default. But what if you wanted to return `401` or `403`? What if you wanted to say to user - "You need to be logged in for viewing current resource" or "You don't have permissions for viewing current resource"?
`ExtenedDjangoObjectPermissions` will help you to be more flexible. By default it behaves as standard [DjangoObjectPermissions](http://www.django-rest-framework.org/api-guide/permissions#djangoobjectpermissions). For example, it is safe to replace `DjangoObjectPermissions` with extended permissions class:
from rest_framework_extensions.permissions import (
ExtendedDjangoObjectPermissions as DjangoObjectPermissions
)
class CommentView(viewsets.ModelViewSet):
permission_classes = (DjangoObjectPermissions,)
Now every request from unauthorized user will get `404` response:
# Request
GET /comments/1/ HTTP/1.1
Accept: application/json
# Response
HTTP/1.1 404 NOT FOUND
Content-Type: application/json; charset=UTF-8
{"detail": "Not found"}
With `ExtenedDjangoObjectPermissions` you can disable hiding forbidden for read objects by changing `hide_forbidden_for_read_objects` attribute:
from rest_framework_extensions.permissions import (
ExtendedDjangoObjectPermissions
)
class CommentViewObjectPermissions(ExtendedDjangoObjectPermissions):
hide_forbidden_for_read_objects = False
class CommentView(viewsets.ModelViewSet):
permission_classes = (CommentViewObjectPermissions,)
Now lets see request response for user that has no permissions for viewing `CommentView` object:
# Request
GET /comments/1/ HTTP/1.1
Accept: application/json
# Response
HTTP/1.1 403 FORBIDDEN
Content-Type: application/json; charset=UTF-8
{u'detail': u'You do not have permission to perform this action.'}
`ExtenedDjangoObjectPermissions` could be used only with Django Rest Framework >= 2.3.8, because [DjangoObjectPermissions](http://www.django-rest-framework.org/topics/release-notes#238) was introduced in 2.3.8 version.
### Caching
To cache something is to save the result of an expensive calculation so that you don't have to perform the calculation next time. Here's some pseudocode explaining how this would work for a dynamically generated api response:
given a URL, try finding that API response in the cache
if the response is in the cache:
return the cached response
else:
generate the response
save the generated response in the cache (for next time)
return the generated response
#### Cache response
DRF-extensions allows you to cache api responses with simple `@cache_response` decorator.
There are two requirements for decorated method:
* It should be method of class which is inherited from `rest_framework.views.APIView`
* It should return `rest_framework.response.Response` instance.
Usage example:
from rest_framework.response import Response
from rest_framework import views
from rest_framework_extensions.cache.decorators import (
cache_response
)
from myapp.models import City
class CityView(views.APIView):
@cache_response()
def get(self, request, *args, **kwargs):
cities = City.objects.all().values_list('name', flat=True)
return Response(cities)
If you request view first time you'll get it from processed SQL query. (~60ms response time):
# Request
GET /cities/ HTTP/1.1
Accept: application/json
# Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
['Moscow', 'London', 'Paris']
Second request will hit the cache. No sql evaluation, no database query. (~30 ms response time):
# Request
GET /cities/ HTTP/1.1
Accept: application/json
# Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
['Moscow', 'London', 'Paris']
Reduction in response time depends on calculation complexity inside your API method. Sometimes it reduces from 1 second to 10ms, sometimes you win just 10ms.
*New in DRF-extensions 0.4.0*
The decorator will render and discard the original DRF response in favor of Django's `HttpResponse`. This allows the cache to retain a smaller memory footprint and eliminates the need to re-render responses on each request. Furthermore it eliminates the risk for users to unknowingly cache whole Serializers and QuerySets.
You can disable this behavior in your test suite by using [dummy caching](https://docs.djangoproject.com/en/stable/topics/cache/#dummy-caching-for-development) for the DRF-extensions cache (set via `DEFAULT_USE_CACHE`).
#### Timeout
You can specify cache timeout in seconds, providing first argument:
class CityView(views.APIView):
@cache_response(60 * 15)
def get(self, request, *args, **kwargs):
...
In the above example, the result of the `get()` view will be cached for 15 minutes.
If you don't specify `timeout` argument then value from `REST_FRAMEWORK_EXTENSIONS` settings will be used. By default it's `None`, which means "cache forever". You can change this default in settings:
REST_FRAMEWORK_EXTENSIONS = {
'DEFAULT_CACHE_RESPONSE_TIMEOUT': 60 * 15
}
#### Usage of the specific cache
*New in DRF-extensions 0.2.3*
`@cache_response` can also take an optional keyword argument, `cache`, which directs the decorator
to use a specific cache (from your [CACHES](https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-CACHES) setting) when caching results.
By default, the `default` cache will be used, but you can specify any cache you want:
class CityView(views.APIView):
@cache_response(60 * 15, cache='special_cache')
def get(self, request, *args, **kwargs):
...
You can specify what cache to use by default in settings:
REST_FRAMEWORK_EXTENSIONS = {
'DEFAULT_USE_CACHE': 'special_cache'
}
#### Cache key
By default every cached data from `@cache_response` decorator stored by key, which calculated
with [DefaultKeyConstructor](#default-key-constructor).
You can change cache key by providing `key_func` argument, which must be callable:
def calculate_cache_key(view_instance, view_method,
request, args, kwargs):
return '.'.join([
len(args),
len(kwargs)
])
class CityView(views.APIView):
@cache_response(60 * 15, key_func=calculate_cache_key)
def get(self, request, *args, **kwargs):
...
You can implement view method and use it for cache key calculation by specifying `key_func` argument as string:
class CityView(views.APIView):
@cache_response(60 * 15, key_func='calculate_cache_key')
def get(self, request, *args, **kwargs):
...
def calculate_cache_key(self, view_instance, view_method,
request, args, kwargs):
return '.'.join([
len(args),
len(kwargs)
])
Key calculation function will be called with next parameters:
* **view_instance** - view instance of decorated method
* **view_method** - decorated method
* **request** - decorated method request
* **args** - decorated method positional arguments
* **kwargs** - decorated method keyword arguments
#### Default key function
If `@cache_response` decorator used without key argument then default key function will be used. You can change this function in
settings:
REST_FRAMEWORK_EXTENSIONS = {
'DEFAULT_CACHE_KEY_FUNC':
'rest_framework_extensions.utils.default_cache_key_func'
}
`default_cache_key_func` uses [DefaultKeyConstructor](#default-key-constructor) as a base for key calculation.
#### Caching errors
*New in DRF-extensions 0.2.7*
By default every response is cached, even failed. For example:
class CityView(views.APIView):
@cache_response()
def get(self, request, *args, **kwargs):
raise Exception("500 error comes from here")
First request to `CityView.get` will fail with `500` status code error and next requests to this endpoint will
return `500` error from cache.
You can change this behaviour by turning off caching error responses:
class CityView(views.APIView):
@cache_response(cache_errors=False)
def get(self, request, *args, **kwargs):
raise Exception("500 error comes from here")
You can change default behaviour by changing `DEFAULT_CACHE_ERRORS` setting:
REST_FRAMEWORK_EXTENSIONS = {
'DEFAULT_CACHE_ERRORS': False
}
#### CacheResponseMixin
It is common to cache standard [viewset](http://www.django-rest-framework.org/api-guide/viewsets) `retrieve` and `list`
methods. That is why `CacheResponseMixin` exists. Just mix it into viewset implementation and those methods will
use functions, defined in `REST_FRAMEWORK_EXTENSIONS` [settings](#settings):
* *"DEFAULT\_OBJECT\_CACHE\_KEY\_FUNC"* for `retrieve` method
* *"DEFAULT\_LIST\_CACHE\_KEY\_FUNC"* for `list` method
By default those functions are using [DefaultKeyConstructor](#default-key-constructor) and extends it:
* With `RetrieveSqlQueryKeyBit` for *"DEFAULT\_OBJECT\_CACHE\_KEY\_FUNC"*
* With `ListSqlQueryKeyBit` and `PaginationKeyBit` for *"DEFAULT\_LIST\_CACHE\_KEY\_FUNC"*
You can change those settings for custom cache key generation:
REST_FRAMEWORK_EXTENSIONS = {
'DEFAULT_OBJECT_CACHE_KEY_FUNC':
'rest_framework_extensions.utils.default_object_cache_key_func',
'DEFAULT_LIST_CACHE_KEY_FUNC':
'rest_framework_extensions.utils.default_list_cache_key_func',
'DEFAULT_CACHE_RESPONSE_TIMEOUT': None,
}
Mixin example usage:
from myapps.serializers import UserSerializer
from rest_framework_extensions.cache.mixins import CacheResponseMixin
class UserViewSet(CacheResponseMixin, viewsets.ModelViewSet):
serializer_class = UserSerializer
You can change cache key function by providing `object_cache_key_func` or
`list_cache_key_func` methods in view class:
class UserViewSet(CacheResponseMixin, viewsets.ModelViewSet):
serializer_class = UserSerializer
def object_cache_key_func(self, **kwargs):
return 'some key for object'
def list_cache_key_func(self, **kwargs):
return 'some key for list'
Of course you can use custom [key constructor](#key-constructor):
from yourapp.key_constructors import (
CustomObjectKeyConstructor,
CustomListKeyConstructor,
)
class UserViewSet(CacheResponseMixin, viewsets.ModelViewSet):
serializer_class = UserSerializer
object_cache_key_func = CustomObjectKeyConstructor()
list_cache_key_func = CustomListKeyConstructor()
*New in DRF-extensions development*
You can change cache timeout by providing `object_cache_timeout` or
`list_cache_timeout` properties in view class:
class UserViewSet(CacheResponseMixin, viewsets.ModelViewSet):
serializer_class = UserSerializer
object_cache_timeout = 3600 # one hours (in seconds)
list_cache_timeout = 60 # one minute (in seconds)
If you want to cache only `retrieve` method then you could use `rest_framework_extensions.cache.mixins.RetrieveCacheResponseMixin`.
If you want to cache only `list` method then you could use `rest_framework_extensions.cache.mixins.ListCacheResponseMixin`.
### Key constructors
As you could see from previous section cache key calculation might seem fairly simple operation. But let's see next example. We make ordinary HTTP request to cities resource:
# Request
GET /cities/ HTTP/1.1
Accept: application/json
# Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
['Moscow', 'London', 'Paris']
By the moment all goes fine - response returned and cached. Let's make the same request requiring XML response:
# Request
GET /cities/ HTTP/1.1
Accept: application/xml
# Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
['Moscow', 'London', 'Paris']
What is that? Oh, we forgot about format negotiations. We can add format to key bits:
def calculate_cache_key(view_instance, view_method,
request, args, kwargs):
return '.'.join([
len(args),
len(kwargs),
request.accepted_renderer.format # here it is
])
# Request
GET /cities/ HTTP/1.1
Accept: application/xml
# Response
HTTP/1.1 200 OK
Content-Type: application/xml; charset=UTF-8
Moscow
London
Paris
That's cool now - we have different responses for different formats with different cache keys. But there are many cases, where key should be different for different requests:
* Response format (json, xml);
* User (exact authorized user or anonymous);
* Different request meta data (request.META['REMOTE_ADDR']);
* Language (ru, en);
* Headers;
* Query params. For example, `jsonp` resources need `callback` param, which rendered in response;
* Pagination. We should show different data for different pages;
* Etc...
Of course we can use custom `calculate_cache_key` methods and reuse them for different API methods, but we can't reuse just parts of them. For example, one method depends on user id and language, but another only on user id. How to be more DRYish? Let's see some magic:
from rest_framework_extensions.key_constructor.constructors import (
KeyConstructor
)
from rest_framework_extensions.key_constructor import bits
from your_app.utils import get_city_by_ip
class CityGetKeyConstructor(KeyConstructor):
unique_method_id = bits.UniqueMethodIdKeyBit()
format = bits.FormatKeyBit()
language = bits.LanguageKeyBit()
class CityHeadKeyConstructor(CityGetKeyConstructor):
user = bits.UserKeyBit()
request_meta = bits.RequestMetaKeyBit(params=['REMOTE_ADDR'])
class CityView(views.APIView):
@cache_response(key_func=CityGetKeyConstructor())
def get(self, request, *args, **kwargs):
cities = City.objects.all().values_list('name', flat=True)
return Response(cities)
@cache_response(key_func=CityHeadKeyConstructor())
def head(self, request, *args, **kwargs):
city = ''
user = self.request.user
if user.is_authenticated and user.city:
city = Response(user.city.name)
if not city:
city = get_city_by_ip(request.META['REMOTE_ADDR'])
return Response(city)
Firstly, let's revise `CityView.get` method cache key calculation. It constructs from 3 bits:
* **unique\_method\_id** - remember our [default key calculation](#cache-key)? Here it is. Just one of the cache key bits. `head` method has different set of bits and they can't collide with `get` method bits. But there could be another view class with the same bits.
* **format** - key would be different for different formats.
* **language** - key would be different for different languages.
The second method `head` has the same `unique_method_id`, `format` and `language` bits, buts extends with 2 more:
* **user** - key would be different for different users. As you can see in response calculation we use `request.user` instance. For different users we need different responses.
* **request_meta** - key would be different for different ip addresses. As you can see in response calculation we are falling back to getting city from ip address if couldn't get it from authorized user model.
All default key bits are listed in [this section](#default-key-bits).
#### Default key constructor
`DefaultKeyConstructor` is located in `rest_framework_extensions.key_constructor.constructors` module and constructs a key
from unique *method* id, request format and request language. It has the following implementation:
class DefaultKeyConstructor(KeyConstructor):
unique_method_id = bits.UniqueMethodIdKeyBit()
format = bits.FormatKeyBit()
language = bits.LanguageKeyBit()
#### How key constructor works
Key constructor class works in the same manner as the standard [django forms](https://docs.djangoproject.com/en/dev/topics/forms/) and
key bits used like form fields. Lets go through key construction steps for [DefaultKeyConstructor](#default-key-constructor).
Firstly, constructor starts iteration over every key bit:
* **unique\_method\_id**
* **format**
* **language**
Then constructor gets data from every key bit calling method `get_data`:
* **unique\_method\_id** - `u'your_app.views.SometView.get'`
* **format** - `u'json'`
* **language** - `u'en'`
Every key bit `get_data` method is called with next arguments:
* **view_instance** - view instance of decorated method
* **view_method** - decorated method
* **request** - decorated method request
* **args** - decorated method positional arguments
* **kwargs** - decorated method keyword arguments
After this it combines every key bit data to one dict, which keys are a key bits names in constructor, and values are returned data:
{
'unique_method_id': u'your_app.views.SometView.get',
'format': u'json',
'language': u'en'
}
Then constructor dumps resulting dict to json:
'{"unique_method_id": "your_app.views.SometView.get", "language": "en", "format": "json"}'
And finally compresses json with **md5** and returns hash value:
'b04f8f03c89df824e0ecd25230a90f0e0ebe184cf8c0114342e9471dd2275baa'
#### Custom key bit
We are going to create a simple key bit which could be used in real applications with next properties:
* High read rate
* Low write rate
The task is - cache every read request and invalidate all cache data after write to any model, which used in API. This approach
let us don't think about granular cache invalidation - just flush it after any model instance change/creation/deletion.
Lets create models:
# models.py
from django.db import models
class Group(models.Model):
title = models.CharField()
class Profile(models.Model):
name = models.CharField()
group = models.ForeignKey(Group)
Define serializers:
# serializers.py
from yourapp.models import Group, Profile
from rest_framework import serializers
class GroupSerializer(serializers.ModelSerializer):
class Meta:
model = Group
class ProfileSerializer(serializers.ModelSerializer):
group = GroupSerializer()
class Meta:
model = Profile
Create views:
# views.py
from yourapp.serializers import GroupSerializer, ProfileSerializer
from yourapp.models import Group, Profile
class GroupViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = GroupSerializer
queryset = Group.objects.all()
class ProfileViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = ProfileSerializer
queryset = Profile.objects.all()
And finally register views in router:
# urls.py
from yourapp.views import GroupViewSet,ProfileViewSet
router = DefaultRouter()
router.register(r'groups', GroupViewSet)
router.register(r'profiles', ProfileViewSet)
urlpatterns = router.urls
At the moment we have API, but it's not cached. Lets cache it and create our custom key bit:
# views.py
import datetime
from django.core.cache import cache
from django.utils.encoding import force_str
from yourapp.serializers import GroupSerializer, ProfileSerializer
from rest_framework_extensions.cache.decorators import cache_response
from rest_framework_extensions.key_constructor.constructors import (
DefaultKeyConstructor
)
from rest_framework_extensions.key_constructor.bits import (
KeyBitBase,
RetrieveSqlQueryKeyBit,
ListSqlQueryKeyBit,
PaginationKeyBit
)
class UpdatedAtKeyBit(KeyBitBase):
def get_data(self, **kwargs):
key = 'api_updated_at_timestamp'
value = cache.get(key, None)
if not value:
value = datetime.datetime.utcnow()
cache.set(key, value=value)
return force_str(value)
class CustomObjectKeyConstructor(DefaultKeyConstructor):
retrieve_sql = RetrieveSqlQueryKeyBit()
updated_at = UpdatedAtKeyBit()
class CustomListKeyConstructor(DefaultKeyConstructor):
list_sql = ListSqlQueryKeyBit()
pagination = PaginationKeyBit()
updated_at = UpdatedAtKeyBit()
class GroupViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = GroupSerializer
@cache_response(key_func=CustomObjectKeyConstructor())
def retrieve(self, *args, **kwargs):
return super(GroupViewSet, self).retrieve(*args, **kwargs)
@cache_response(key_func=CustomListKeyConstructor())
def list(self, *args, **kwargs):
return super(GroupViewSet, self).list(*args, **kwargs)
class ProfileViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = ProfileSerializer
@cache_response(key_func=CustomObjectKeyConstructor())
def retrieve(self, *args, **kwargs):
return super(ProfileViewSet, self).retrieve(*args, **kwargs)
@cache_response(key_func=CustomListKeyConstructor())
def list(self, *args, **kwargs):
return super(ProfileViewSet, self).list(*args, **kwargs)
As you can see `UpdatedAtKeyBit` just adds to key information when API models has been update last time. If there is no
information about it then new datetime will be used for key bit data.
Lets write cache invalidation. We just connect models to standard signals and change value in cache by key `api_updated_at_timestamp`:
# models.py
import datetime
from django.db import models
from django.db.models.signals import post_save, post_delete
def change_api_updated_at(sender=None, instance=None, *args, **kwargs):
cache.set('api_updated_at_timestamp', datetime.datetime.utcnow())
class Group(models.Model):
title = models.CharField()
class Profile(models.Model):
name = models.CharField()
group = models.ForeignKey(Group)
for model in [Group, Profile]:
post_save.connect(receiver=change_api_updated_at, sender=model)
post_delete.connect(receiver=change_api_updated_at, sender=model)
And that's it. When any model changes then value in cache by key `api_updated_at_timestamp` will be changed too. After this every
key constructor, that used `UpdatedAtKeyBit`, will construct new keys and `@cache_response` decorator will
cache data in new places.
#### Key constructor params
*New in DRF-extensions 0.2.3*
You can change `params` attribute for specific key bit by providing `params` dict for key constructor initialization
function. For example, here is custom key constructor, which inherits from [DefaultKeyConstructor](#default-key-constructor)
and adds geoip key bit:
class CityKeyConstructor(DefaultKeyConstructor):
geoip = bits.RequestMetaKeyBit(params=['GEOIP_CITY'])
If you wanted to use `GEOIP_COUNTRY`, you could create new key constructor:
class CountryKeyConstructor(DefaultKeyConstructor):
geoip = bits.RequestMetaKeyBit(params=['GEOIP_COUNTRY'])
But there is another way. You can send `params` in key constructor initialization method. This is the dict attribute, where
keys are bit names and values are bit `params` attribute value (look at `CountryView`):
class CityKeyConstructor(DefaultKeyConstructor):
geoip = bits.RequestMetaKeyBit(params=['GEOIP_CITY'])
class CityView(views.APIView):
@cache_response(key_func=CityKeyConstructor())
def get(self, request, *args, **kwargs):
...
class CountryView(views.APIView):
@cache_response(key_func=CityKeyConstructor(
params={'geoip': ['GEOIP_COUNTRY']}
))
def get(self, request, *args, **kwargs):
...
If there is no item provided for key bit then default key bit `params` value will be used.
#### Constructor's bits list
You can dynamically change key constructor's bits list in initialization method by altering `bits` attribute:
class CustomKeyConstructor(DefaultKeyConstructor):
def __init__(self, *args, **kwargs):
super(CustomKeyConstructor, self).__init__(*args, **kwargs)
self.bits['geoip'] = bits.RequestMetaKeyBit(
params=['GEOIP_CITY']
)
### Default key bits
Out of the box DRF-extensions has some basic key bits. They are all located in `rest_framework_extensions.key_constructor.bits` module.
#### FormatKeyBit
Retrieves format info from request. Usage example:
class MyKeyConstructor(KeyConstructor):
format = FormatKeyBit()
#### LanguageKeyBit
Retrieves active language for request. Usage example:
class MyKeyConstructor(KeyConstructor):
language = LanguageKeyBit()
#### UserKeyBit
Retrieves user id from request. If it is anonymous then returnes *"anonymous"* string. Usage example:
class MyKeyConstructor(KeyConstructor):
user = UserKeyBit()
#### RequestMetaKeyBit
Retrieves data from [request.META](https://docs.djangoproject.com/en/dev/ref/request-response/#django.http.HttpRequest.META) dict.
Usage example:
class MyKeyConstructor(KeyConstructor):
ip_address_and_user_agent = bits.RequestMetaKeyBit(
['REMOTE_ADDR', 'HTTP_USER_AGENT']
)
You can use `*` for retrieving all meta data to key bit:
*New in DRF-extensions 0.2.7*
class MyKeyConstructor(KeyConstructor):
all_request_meta = bits.RequestMetaKeyBit('*')
#### HeadersKeyBit
Same as `RequestMetaKeyBit` retrieves data from [request.META](https://docs.djangoproject.com/en/dev/ref/request-response/#django.http.HttpRequest.META) dict.
The difference is that `HeadersKeyBit` allows to use normal header names:
class MyKeyConstructor(KeyConstructor):
user_agent_and_geobase_id = bits.HeadersKeyBit(
['user-agent', 'x-geobase-id']
)
# will process request.META['HTTP_USER_AGENT'] and
# request.META['HTTP_X_GEOBASE_ID']
You can use `*` for retrieving all headers to key bit:
*New in DRF-extensions 0.2.7*
class MyKeyConstructor(KeyConstructor):
all_headers = bits.HeadersKeyBit('*')
#### ArgsKeyBit
*New in DRF-extensions 0.2.7*
Retrieves data from the view's positional arguments.
A list of position indices can be passed to indicate which arguments to use. For retrieving all arguments you can use `*` which is also the default value:
class MyKeyConstructor(KeyConstructor):
args = bits.ArgsKeyBit() # will use all positional arguments
class MyKeyConstructor(KeyConstructor):
args = bits.ArgsKeyBit('*') # same as above
class MyKeyConstructor(KeyConstructor):
args = bits.ArgsKeyBit([0, 2])
#### KwargsKeyBit
*New in DRF-extensions 0.2.7*
Retrieves data from the views's keyword arguments.
A list of keyword argument names can be passed to indicate which kwargs to use. For retrieving all kwargs you can use `*` which is also the default value:
class MyKeyConstructor(KeyConstructor):
kwargs = bits.KwargsKeyBit() # will use all keyword arguments
class MyKeyConstructor(KeyConstructor):
kwargs = bits.KwargsKeyBit('*') # same as above
class MyKeyConstructor(KeyConstructor):
kwargs = bits.KwargsKeyBit(['user_id', 'city'])
#### QueryParamsKeyBit
Retrieves data from [request.GET](https://docs.djangoproject.com/en/dev/ref/request-response/#django.http.HttpRequest.GET) dict.
Usage example:
class MyKeyConstructor(KeyConstructor):
part_and_callback = bits.QueryParamsKeyBit(
['part', 'callback']
)
You can use `*` for retrieving all query params to key bit which is also the default value:
*New in DRF-extensions 0.2.7*
class MyKeyConstructor(KeyConstructor):
all_query_params = bits.QueryParamsKeyBit('*') # all qs parameters
class MyKeyConstructor(KeyConstructor):
all_query_params = bits.QueryParamsKeyBit() # same as above
#### PaginationKeyBit
Inherits from `QueryParamsKeyBit` and returns data from used pagination params.
class MyKeyConstructor(KeyConstructor):
pagination = bits.PaginationKeyBit()
#### ListSqlQueryKeyBit
Retrieves sql query for `view.filter_queryset(view.get_queryset())` filtering.
class MyKeyConstructor(KeyConstructor):
list_sql_query = bits.ListSqlQueryKeyBit()
#### RetrieveSqlQueryKeyBit
Retrieves sql query for retrieving exact object.
class MyKeyConstructor(KeyConstructor):
retrieve_sql_query = bits.RetrieveSqlQueryKeyBit()
#### UniqueViewIdKeyBit
Combines data about view module and view class name.
class MyKeyConstructor(KeyConstructor):
unique_view_id = bits.UniqueViewIdKeyBit()
#### UniqueMethodIdKeyBit
Combines data about view module, view class name and view method name.
class MyKeyConstructor(KeyConstructor):
unique_view_id = bits.UniqueMethodIdKeyBit()
#### ListModelKeyBit
*New in DRF-extensions 0.3.2*
Computes the semantic fingerprint of a list of objects returned by `view.filter_queryset(view.get_queryset())`
using a flat representation of all objects' values.
class MyKeyConstructor(KeyConstructor):
list_model_values = bits.ListModelKeyBit()
#### RetrieveModelKeyBit
*New in DRF-extensions 0.3.2*
Computes the semantic fingerprint of a particular objects returned by `view.get_object()`.
class MyKeyConstructor(KeyConstructor):
retrieve_model_values = bits.RetrieveModelKeyBit()
### Conditional requests
The etag functionality is pending an overhaul has been temporarily removed since 0.4.0.
See discussion in [Issue #177](https://github.com/chibisov/drf-extensions/issues/177)
### Bulk operations
*New in DRF-extensions 0.2.4*
Bulk operations allows you to perform operations over set of objects with one request. There is third-party package
[django-rest-framework-bulk](django-rest-framework-bulk) with support for all CRUD methods, but it iterates over every
instance in bulk operation, serializes it and only after that executes operation.
It plays nice with `create` or `update`
operations, but becomes unacceptable with `partial update` and `delete` methods over the `QuerySet`. Such kind of
`QuerySet` could contain thousands of objects and should be performed as database query over the set at once.
Please note - DRF-extensions bulk operations applies over `QuerySet`, not over instances. It means that:
* No serializer's `save` or `delete` methods would be called
* No viewset's `pre_save`, `post_save`, `pre_delete` and `post_delete` would be called
* No model signals would be called
#### Safety
Bulk operations are very dangerous in case of making stupid mistakes. For example you wanted to delete user instance
with `DELETE` request from your client application.
# Request
DELETE /users/1/ HTTP/1.1
Accept: application/json
# Response
HTTP/1.1 204 NO CONTENT
Content-Type: application/json; charset=UTF-8
That was example of successful deletion. But there is the common situation when client could not get instance id and sends
request to endpoint without it:
# Request
DELETE /users/ HTTP/1.1
Accept: application/json
# Response
HTTP/1.1 204 NO CONTENT
Content-Type: application/json; charset=UTF-8
If you used [bulk destroy mixin](#bulk-destroy) for `/users/` endpoint, then all your user objects would be deleted.
To protect from such confusions DRF-extensions asks you to send `X-BULK-OPERATION` header
for every bulk operation request. With this protection previous example would not delete any user instances:
# Request
DELETE /users/ HTTP/1.1
Accept: application/json
# Response
HTTP/1.1 400 BAD REQUEST
Content-Type: application/json; charset=UTF-8
{
"detail": "Header 'X-BULK-OPERATION' should be provided for bulk operation."
}
With `X-BULK-OPERATION` header it works as expected - deletes all user instances:
# Request
DELETE /users/ HTTP/1.1
Accept: application/json
X-BULK-OPERATION: true
# Response
HTTP/1.1 204 NO CONTENT
Content-Type: application/json; charset=UTF-8
You can change bulk operation header name in settings:
REST_FRAMEWORK_EXTENSIONS = {
'DEFAULT_BULK_OPERATION_HEADER_NAME': 'X-CUSTOM-BULK-OPERATION'
}
To turn off protection you can set `DEFAULT_BULK_OPERATION_HEADER_NAME` as `None`.
#### Bulk destroy
This mixin allows you to delete many instances with one `DELETE` request.
from rest_framework_extensions.bulk_operations.mixins import ListDestroyModelMixin
class UserViewSet(ListDestroyModelMixin, viewsets.ModelViewSet):
serializer_class = UserSerializer
Bulk destroy example - delete all users which emails ends with `gmail.com`:
# Request
DELETE /users/?email__endswith=gmail.com HTTP/1.1
Accept: application/json
X-BULK-OPERATION: true
# Response
HTTP/1.1 204 NO CONTENT
Content-Type: application/json; charset=UTF-8
#### Bulk update
This mixin allows you to update many instances with one `PATCH` request. Note, that this mixin works only with partial update.
from rest_framework_extensions.mixins import ListUpdateModelMixin
class UserViewSet(ListUpdateModelMixin, viewsets.ModelViewSet):
serializer_class = UserSerializer
Bulk partial update example - set `email_provider` of every user as `google`, if it's email ends with `gmail.com`:
# Request
PATCH /users/?email__endswith=gmail.com HTTP/1.1
Accept: application/json
X-BULK-OPERATION: true
{"email_provider": "google"}
# Response
HTTP/1.1 204 NO CONTENT
Content-Type: application/json; charset=UTF-8
### Settings
DRF-extensions follows Django Rest Framework approach in settings implementation.
[In Django Rest Framework](http://www.django-rest-framework.org/api-guide/settings) you specify custom settings by changing `REST_FRAMEWORK` variable in settings file:
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.YAMLRenderer',
),
'DEFAULT_PARSER_CLASSES': (
'rest_framework.parsers.YAMLParser',
)
}
In DRF-extensions there is a magic variable too called `REST_FRAMEWORK_EXTENSIONS`:
REST_FRAMEWORK_EXTENSIONS = {
'DEFAULT_CACHE_RESPONSE_TIMEOUT': 60 * 15
}
#### Accessing settings
If you need to access the values of DRF-extensions API settings in your project, you should use the `extensions_api_settings` object. For example:
from rest_framework_extensions.settings import extensions_api_settings
print extensions_api_settings.DEFAULT_CACHE_RESPONSE_TIMEOUT
### Release notes
You can read about versioning, deprecation policy and upgrading from
[Django REST framework documentation](https://www.django-rest-framework.org/community/release-notes/).
#### 0.7.1
* Added support for Django 3.2
* Dropped drf 3.11
#### 0.7.0
* Added support for Django 3.1
* Dropped support below Django 2.2
* Added support for DRF 3.12
* fix(PartialUpdateSerializerMixin): support nesting on same instance
#### 0.6.0
*Jan 27, 2020*
* Added support for Django 3.0 (#276)
* Dropped support for Django 2.0
* Added support for DRF 3.10 and 3.11 (#261, #279)
* Added support for Python 3.8 (#282)
* Added paginate decorator (#266)
* Added limit/offset and cursor pagination to PaginationKeyBit (#204)
#### 0.5.0
*May 10, 2019*
* Dropped python 2.7 and 3.4
* Fix possible header mutation issue
* Added ability to [use a specific cache timeouts](#cacheresponsemixin) for `CacheResponseMixin`
* Test against Django 2.1, DRF 3.9 and django-filter 2.0.0
* Dropped support of older DRF version lower than 3.9
* Django 2.2 support added
#### 0.4.0
*Sep 5, 2018*
* Added support for django 1.11 and 2.0
* Dropped support for django versions lower then 1.11
* Nested routes with over 2 levels now respect `lookup_value_regex`
* Added support for DRF 3.8
* Dropped support of older DRF version lower then 3.8
* Cache only the renered response instead of rendering whole response object
* The etag functionalties are not enabled by default, have to enable it manually
#### 0.3.2
*Jan 4, 2017*
* Added `rest_framework_extensions.exceptions.PreconditionRequiredException` as subclass of `rest_framework.exceptions.APIException`
* Added `@api_etag` decorator function and `APIETAGProcessor` that uses *semantic* ETags per API resource, decoupled from views, such that it can be used in optimistic concurrency control
* Added new default key bits `RetrieveModelKeyBit` and `ListModelKeyBit` for computing the semantic fingerprint of a django model instance
* Added `APIETAGMixin` to be used in DRF viewsets and views
* Added new settings for default implementation of the API ETag functions: `DEFAULT_API_OBJECT_ETAG_FUNC`, `DEFAULT_API_LIST_ETAG_FUNC`
* Added test application for functional tests and demo as `tests_app/tests/functional/concurrency/conditional_request`
* Added unit tests for the `@api_etag` decorator
* DRF 3.5.x, Django pre-1.10 compatibility of the key bit construction
* (Test-)Code cleanup
#### 0.3.1
*Sep 29, 2016*
* Fix `schema_urls` `ExtendedDefaultRouter` compatibility issue introduced by DRF 3.4.0
* Removed deprecated @action() and @link() decorators
* DRF 3.4.x compatibility
* Django 1.9 and 1.10 compatibility
#### 0.2.8
*Sep 21, 2015*
* Fixed `ListSqlQueryKeyBit` and `RetrieveSqlQueryKeyBit` [problems](https://github.com/chibisov/drf-extensions/issues/28) with `EmptyResultSet` exception ([pull](https://github.com/chibisov/drf-extensions/pull/75/)).
* All items are now by default in [ArgsKeyBit](#argskeybit), [KwargsKeyBit](#kwargskeybit) and [QueryParamsKeyBit](#queryparamskeybit)
* Respect parent lookup regex value for [Nested routes](#nested-routes) ([issue](https://github.com/chibisov/drf-extensions/pull/87)).
#### 0.2.7
*Feb 2, 2015*
* [DRF 3.x compatibility](https://github.com/chibisov/drf-extensions/issues/39)
* [DetailSerializerMixin](#detailserializermixin) is now [compatible with DRF 3.0](https://github.com/chibisov/drf-extensions/issues/46)
* Added [ArgsKeyBit](#argskeybit)
* Added [KwargsKeyBit](#kwargskeybit)
* Fixed [PartialUpdateSerializerMixin](#partialupdateserializermixin) [compatibility issue with DRF 3.x](https://github.com/chibisov/drf-extensions/issues/66)
* Added [cache_errors](#caching-errors) attribute for switching caching for error responses
* Added ability to specify usage of all items for [RequestMetaKeyBit](#requestmetakeybit), [HeadersKeyBit](#headerskeybit)
and [QueryParamsKeyBit](#queryparamskeybit) providing `params='*'`
* [Collection level controllers](#collection-level-controllers) is in pending deprecation
* [Controller endpoint name](#controller-endpoint-name) is in pending deprecation
#### 0.2.6
*Sep 9, 2014*
* Usage of [django.core.cache.caches](https://docs.djangoproject.com/en/dev/topics/cache/#django.core.cache.caches) for
django >= 1.7
* [Documented ETag usage with GZipMiddleware](#gzipped-etags)
* Fixed `ListSqlQueryKeyBit` and `RetrieveSqlQueryKeyBit` [problems](https://github.com/chibisov/drf-extensions/issues/28)
with `EmptyResultSet`.
* Fixed [cache response](#cache-response) compatibility [issue](https://github.com/chibisov/drf-extensions/issues/32)
with DRF 2.4.x
#### 0.2.5
*July 9, 2014*
* Fixed [setuptools confusion with pyc files](https://github.com/chibisov/drf-extensions/issues/20)
#### 0.2.4
*July 7, 2014*
* Added tests for [Django REST Framework 2.3.14](http://www.django-rest-framework.org/topics/release-notes#2314)
* Added [Bulk operations](#bulk-operations)
* Fixed [extended routers](#routers) compatibility issue with [default controller decorators](http://www.django-rest-framework.org/api-guide/viewsets#marking-extra-methods-for-routing)
* Documented [pluggable router mixins](#pluggable-router-mixins)
* Added [nested routes](#nested-routes)
#### 0.2.3
*Apr. 25, 2014*
* Added [PartialUpdateSerializerMixin](#partialupdateserializermixin)
* Added [Key constructor params](#key-constructor-params)
* Documented dynamically [constructor's bits list](#constructor-s-bits-list) altering
* Added ability to [use a specific cache](#usage-of-the-specific-cache) for `@cache_response` decorator
#### 0.2.2
*Mar. 23, 2014*
* Added [PaginateByMaxMixin](#paginatebymaxmixin)
* Added [ExtenedDjangoObjectPermissions](#object-permissions)
* Added tests for django 1.7
#### 0.2.1
*Feb. 1, 2014*
* Rewritten tests to nose and tox
* New tests directory structure
* Rewritten HTTP documentation requests examples into more raw manner
* Added trailing_slash on extended routers for Django Rest Framework versions`>=2.3.6` (which supports this feature)
* Added [caching](#caching)
* Added [key constructor](#key-constructor)
* Added [conditional requests](#conditional-requests) with Etag calculation
* Added [Cache/ETAG mixins](#cache-etag-mixins)
* Added [CacheResponseMixin](#cacheresponsemixin)
* Added [ETAGMixin](#etagmixin)
* Documented [ResourceUriField](#resourceurifield)
* Documented [settings](#settings) customization
#### 0.2
*Nov. 5, 2013*
* Moved docs from readme to github pages
* Docs generation with [Backdoc](https://github.com/chibisov/backdoc)
drf-extensions-0.7.1/rest_framework_extensions/ 0000775 0000000 0000000 00000000000 14100726230 0021773 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/rest_framework_extensions/__init__.py 0000664 0000000 0000000 00000000073 14100726230 0024104 0 ustar 00root root 0000000 0000000 __version__ = '0.7.1' # from 0.7.0
VERSION = __version__
drf-extensions-0.7.1/rest_framework_extensions/bulk_operations/ 0000775 0000000 0000000 00000000000 14100726230 0025173 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/rest_framework_extensions/bulk_operations/__init__.py 0000664 0000000 0000000 00000000000 14100726230 0027272 0 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/rest_framework_extensions/bulk_operations/mixins.py 0000664 0000000 0000000 00000007712 14100726230 0027063 0 ustar 00root root 0000000 0000000 from django.utils.encoding import force_str
from rest_framework import status
from rest_framework.response import Response
from rest_framework_extensions.settings import extensions_api_settings
from rest_framework_extensions import utils
class BulkOperationBaseMixin:
def is_object_operation(self):
return bool(self.get_object_lookup_value())
def get_object_lookup_value(self):
return self.kwargs.get(getattr(self, 'lookup_url_kwarg', None) or self.lookup_field, None)
def is_valid_bulk_operation(self):
if extensions_api_settings.DEFAULT_BULK_OPERATION_HEADER_NAME:
header_name = utils.prepare_header_name(
extensions_api_settings.DEFAULT_BULK_OPERATION_HEADER_NAME)
return bool(self.request.META.get(header_name, None)), {
'detail': 'Header \'{0}\' should be provided for bulk operation.'.format(
extensions_api_settings.DEFAULT_BULK_OPERATION_HEADER_NAME
)
}
else:
return True, {}
class ListDestroyModelMixin(BulkOperationBaseMixin):
def delete(self, request, *args, **kwargs):
if self.is_object_operation():
return super().destroy(request, *args, **kwargs)
else:
return self.destroy_bulk(request, *args, **kwargs)
def destroy_bulk(self, request, *args, **kwargs):
is_valid, errors = self.is_valid_bulk_operation()
if is_valid:
queryset = self.filter_queryset(self.get_queryset())
self.pre_delete_bulk(queryset) # todo: test and document me
queryset.delete()
self.post_delete_bulk(queryset) # todo: test and document me
return Response(status=status.HTTP_204_NO_CONTENT)
else:
return Response(errors, status=status.HTTP_400_BAD_REQUEST)
def pre_delete_bulk(self, queryset):
"""
Placeholder method for calling before deleting an queryset.
"""
pass
def post_delete_bulk(self, queryset):
"""
Placeholder method for calling after deleting an queryset.
"""
pass
class ListUpdateModelMixin(BulkOperationBaseMixin):
def patch(self, request, *args, **kwargs):
if self.is_object_operation():
return super().partial_update(request, *args, **kwargs)
else:
return self.partial_update_bulk(request, *args, **kwargs)
def partial_update_bulk(self, request, *args, **kwargs):
is_valid, errors = self.is_valid_bulk_operation()
if is_valid:
queryset = self.filter_queryset(self.get_queryset())
update_bulk_dict = self.get_update_bulk_dict(
serializer=self.get_serializer_class()(), data=request.data)
# todo: test and document me
self.pre_save_bulk(queryset, update_bulk_dict)
try:
queryset.update(**update_bulk_dict)
except ValueError as e:
errors = {
'detail': force_str(e)
}
return Response(errors, status=status.HTTP_400_BAD_REQUEST)
# todo: test and document me
self.post_save_bulk(queryset, update_bulk_dict)
return Response(status=status.HTTP_204_NO_CONTENT)
else:
return Response(errors, status=status.HTTP_400_BAD_REQUEST)
def get_update_bulk_dict(self, serializer, data):
update_bulk_dict = {}
for field_name, field in serializer.fields.items():
if field_name in data and not field.read_only:
update_bulk_dict[field.source or field_name] = data[field_name]
return update_bulk_dict
def pre_save_bulk(self, queryset, update_bulk_dict):
"""
Placeholder method for calling before deleting an queryset.
"""
pass
def post_save_bulk(self, queryset, update_bulk_dict):
"""
Placeholder method for calling after deleting an queryset.
"""
pass
drf-extensions-0.7.1/rest_framework_extensions/cache/ 0000775 0000000 0000000 00000000000 14100726230 0023036 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/rest_framework_extensions/cache/__init__.py 0000664 0000000 0000000 00000000000 14100726230 0025135 0 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/rest_framework_extensions/cache/decorators.py 0000664 0000000 0000000 00000010560 14100726230 0025557 0 ustar 00root root 0000000 0000000 from functools import wraps, WRAPPER_ASSIGNMENTS
from django.http.response import HttpResponse
from rest_framework_extensions.settings import extensions_api_settings
def get_cache(alias):
from django.core.cache import caches
return caches[alias]
class CacheResponse:
"""
Store/Receive and return cached `HttpResponse` based on DRF response.
.. note::
This decorator will render and discard the original DRF response in
favor of Django's `HttpResponse`. The allows the cache to retain a
smaller memory footprint and eliminates the need to re-render
responses on each request. Furthermore it eliminates the risk for users
to unknowingly cache whole Serializers and QuerySets.
"""
def __init__(self,
timeout=None,
key_func=None,
cache=None,
cache_errors=None):
if timeout is None:
self.timeout = extensions_api_settings.DEFAULT_CACHE_RESPONSE_TIMEOUT
else:
self.timeout = timeout
if key_func is None:
self.key_func = extensions_api_settings.DEFAULT_CACHE_KEY_FUNC
else:
self.key_func = key_func
if cache_errors is None:
self.cache_errors = extensions_api_settings.DEFAULT_CACHE_ERRORS
else:
self.cache_errors = cache_errors
self.cache = get_cache(cache or extensions_api_settings.DEFAULT_USE_CACHE)
def __call__(self, func):
this = self
@wraps(func, assigned=WRAPPER_ASSIGNMENTS)
def inner(self, request, *args, **kwargs):
return this.process_cache_response(
view_instance=self,
view_method=func,
request=request,
args=args,
kwargs=kwargs,
)
return inner
def process_cache_response(self,
view_instance,
view_method,
request,
args,
kwargs):
key = self.calculate_key(
view_instance=view_instance,
view_method=view_method,
request=request,
args=args,
kwargs=kwargs
)
timeout = self.calculate_timeout(view_instance=view_instance)
response_triple = self.cache.get(key)
if not response_triple:
# render response to create and cache the content byte string
response = view_method(view_instance, request, *args, **kwargs)
response = view_instance.finalize_response(request, response, *args, **kwargs)
response.render()
if not response.status_code >= 400 or self.cache_errors:
# django 3.0 has not .items() method, django 3.2 has not ._headers
if hasattr(response, '_headers'):
headers = response._headers.copy()
else:
headers = {k: (k, v) for k, v in response.items()}
response_triple = (
response.rendered_content,
response.status_code,
headers
)
self.cache.set(key, response_triple, timeout)
else:
# build smaller Django HttpResponse
content, status, headers = response_triple
response = HttpResponse(content=content, status=status)
for k, v in headers.values():
response[k] = v
if not hasattr(response, '_closable_objects'):
response._closable_objects = []
return response
def calculate_key(self,
view_instance,
view_method,
request,
args,
kwargs):
if isinstance(self.key_func, str):
key_func = getattr(view_instance, self.key_func)
else:
key_func = self.key_func
return key_func(
view_instance=view_instance,
view_method=view_method,
request=request,
args=args,
kwargs=kwargs,
)
def calculate_timeout(self, view_instance, **_):
if isinstance(self.timeout, str):
self.timeout = getattr(view_instance, self.timeout)
return self.timeout
cache_response = CacheResponse
drf-extensions-0.7.1/rest_framework_extensions/cache/mixins.py 0000664 0000000 0000000 00000002323 14100726230 0024717 0 ustar 00root root 0000000 0000000 from rest_framework_extensions.cache.decorators import cache_response
from rest_framework_extensions.settings import extensions_api_settings
class BaseCacheResponseMixin:
# todo: test me. Create generic test like
# test_cache_reponse(view_instance, method, should_rebuild_after_method_evaluation)
object_cache_key_func = extensions_api_settings.DEFAULT_OBJECT_CACHE_KEY_FUNC
list_cache_key_func = extensions_api_settings.DEFAULT_LIST_CACHE_KEY_FUNC
object_cache_timeout = extensions_api_settings.DEFAULT_CACHE_RESPONSE_TIMEOUT
list_cache_timeout = extensions_api_settings.DEFAULT_CACHE_RESPONSE_TIMEOUT
class ListCacheResponseMixin(BaseCacheResponseMixin):
@cache_response(key_func='list_cache_key_func', timeout='list_cache_timeout')
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
class RetrieveCacheResponseMixin(BaseCacheResponseMixin):
@cache_response(key_func='object_cache_key_func', timeout='object_cache_timeout')
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
class CacheResponseMixin(RetrieveCacheResponseMixin,
ListCacheResponseMixin):
pass
drf-extensions-0.7.1/rest_framework_extensions/compat.py 0000664 0000000 0000000 00000001057 14100726230 0023633 0 ustar 00root root 0000000 0000000 """
The `compat` module provides support for backwards compatibility with older
versions of django/python, and compatibility wrappers around optional packages.
"""
# handle different QuerySet representations
def queryset_to_value_list(queryset):
assert isinstance(queryset, str)
# django 1.10 introduces syntax ""
# we extract only the list of tuples from the string
idx_bracket_open = queryset.find(u'[')
idx_bracket_close = queryset.rfind(u']')
return queryset[idx_bracket_open:idx_bracket_close + 1]
drf-extensions-0.7.1/rest_framework_extensions/decorators.py 0000664 0000000 0000000 00000001342 14100726230 0024512 0 ustar 00root root 0000000 0000000 def paginate(pagination_class=None, **kwargs):
"""
Decorator that adds a pagination_class to GenericViewSet class.
Custom pagination class also available.
Usage :
from rest_framework.pagination import CursorPagination
@paginate(pagination_class=CursorPagination, page_size=5, ordering='-created_at')
class FooViewSet(viewsets.GenericViewSet):
...
"""
assert pagination_class is not None, (
"@paginate missing required argument: 'pagination_class'"
)
class _Pagination(pagination_class):
def __init__(self):
self.__dict__.update(kwargs)
def decorator(_class):
_class.pagination_class = _Pagination
return _class
return decorator
drf-extensions-0.7.1/rest_framework_extensions/etag/ 0000775 0000000 0000000 00000000000 14100726230 0022713 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/rest_framework_extensions/etag/__init__.py 0000664 0000000 0000000 00000000001 14100726230 0025013 0 ustar 00root root 0000000 0000000
drf-extensions-0.7.1/rest_framework_extensions/etag/decorators.py 0000664 0000000 0000000 00000021512 14100726230 0025433 0 ustar 00root root 0000000 0000000 import logging
from functools import wraps, WRAPPER_ASSIGNMENTS
from django.utils.http import parse_etags, quote_etag
from rest_framework import status
from rest_framework.permissions import SAFE_METHODS
from rest_framework.response import Response
from rest_framework_extensions.exceptions import PreconditionRequiredException
from rest_framework_extensions.utils import prepare_header_name
from rest_framework_extensions.settings import extensions_api_settings
logger = logging.getLogger('django.request')
class ETAGProcessor:
"""Based on https://github.com/django/django/blob/master/django/views/decorators/http.py"""
def __init__(self, etag_func=None, rebuild_after_method_evaluation=False):
if not etag_func:
etag_func = extensions_api_settings.DEFAULT_ETAG_FUNC
self.etag_func = etag_func
self.rebuild_after_method_evaluation = rebuild_after_method_evaluation
def __call__(self, func):
this = self
@wraps(func, assigned=WRAPPER_ASSIGNMENTS)
def inner(self, request, *args, **kwargs):
return this.process_conditional_request(
view_instance=self,
view_method=func,
request=request,
args=args,
kwargs=kwargs,
)
return inner
def process_conditional_request(self,
view_instance,
view_method,
request,
args,
kwargs):
etags, if_none_match, if_match = self.get_etags_and_matchers(request)
res_etag = self.calculate_etag(
view_instance=view_instance,
view_method=view_method,
request=request,
args=args,
kwargs=kwargs,
)
if self.is_if_none_match_failed(res_etag, etags, if_none_match):
if request.method in SAFE_METHODS:
response = Response(status=status.HTTP_304_NOT_MODIFIED)
else:
response = self._get_and_log_precondition_failed_response(
request=request)
elif self.is_if_match_failed(res_etag, etags, if_match):
response = self._get_and_log_precondition_failed_response(
request=request)
else:
response = view_method(view_instance, request, *args, **kwargs)
if self.rebuild_after_method_evaluation:
res_etag = self.calculate_etag(
view_instance=view_instance,
view_method=view_method,
request=request,
args=args,
kwargs=kwargs,
)
if res_etag and not response.has_header('ETag'):
response['ETag'] = quote_etag(res_etag)
return response
def get_etags_and_matchers(self, request):
etags = None
if_none_match = request.META.get(prepare_header_name("if-none-match"))
if_match = request.META.get(prepare_header_name("if-match"))
if if_none_match or if_match:
# There can be more than one ETag in the request, so we
# consider the list of values.
try:
etags = parse_etags(if_none_match or if_match)
except ValueError:
# In case of invalid etag ignore all ETag headers.
# Apparently Opera sends invalidly quoted headers at times
# (we should be returning a 400 response, but that's a
# little extreme) -- this is Django bug #10681.
if_none_match = None
if_match = None
return etags, if_none_match, if_match
def calculate_etag(self,
view_instance,
view_method,
request,
args,
kwargs):
if isinstance(self.etag_func, str):
etag_func = getattr(view_instance, self.etag_func)
else:
etag_func = self.etag_func
return etag_func(
view_instance=view_instance,
view_method=view_method,
request=request,
args=args,
kwargs=kwargs,
)
def is_if_none_match_failed(self, res_etag, etags, if_none_match):
if res_etag and if_none_match:
etags = [etag.strip('"') for etag in etags]
return res_etag in etags or '*' in etags
else:
return False
def is_if_match_failed(self, res_etag, etags, if_match):
if res_etag and if_match:
return res_etag not in etags and '*' not in etags
else:
return False
def _get_and_log_precondition_failed_response(self, request):
logger.warning('Precondition Failed: %s', request.path,
extra={
'status_code': status.HTTP_412_PRECONDITION_FAILED,
'request': request
}
)
return Response(status=status.HTTP_412_PRECONDITION_FAILED)
class APIETAGProcessor(ETAGProcessor):
"""
This class is responsible for calculating the ETag value given (a list of) model instance(s).
It does not make sense to compute a default ETag here, because the processor would always issue a 304 response,
even if the response was modified meanwhile.
Therefore the `APIETAGProcessor` cannot be used without specifying an `etag_func` as keyword argument.
According to RFC 6585, conditional headers may be enforced for certain services that support conditional
requests. For optimistic locking, the server should respond status code 428 including a description on how
to resubmit the request successfully, see https://tools.ietf.org/html/rfc6585#section-3.
"""
# require a pre-conditional header (e.g. If-Match) for unsafe HTTP methods (RFC 6585)
# override this defaults, if required
precondition_map = {'PUT': ['If-Match'],
'PATCH': ['If-Match'],
'DELETE': ['If-Match']}
def __init__(self, etag_func=None, rebuild_after_method_evaluation=False, precondition_map=None):
assert etag_func is not None, ('None-type functions are not allowed for processing API ETags.'
'You must specify a proper function to calculate the API ETags '
'using the "etag_func" keyword argument.')
if precondition_map is not None:
self.precondition_map = precondition_map
assert isinstance(self.precondition_map, dict), ('`precondition_map` must be a dict, where '
'the key is the HTTP verb, and the value is a list of '
'HTTP headers that must all be present for that request.')
super().__init__(etag_func=etag_func,
rebuild_after_method_evaluation=rebuild_after_method_evaluation)
def get_etags_and_matchers(self, request):
"""Get the etags from the header and perform a validation against the required preconditions."""
# evaluate the preconditions, raises 428 if condition is not met
self.evaluate_preconditions(request)
# alright, headers are present, extract the values and match the conditions
return super().get_etags_and_matchers(request)
def evaluate_preconditions(self, request):
"""Evaluate whether the precondition for the request is met."""
if request.method.upper() in self.precondition_map.keys():
required_headers = self.precondition_map.get(
request.method.upper(), [])
# check the required headers
for header in required_headers:
if not request.META.get(prepare_header_name(header)):
# raise an error for each header that does not match
logger.warning('Precondition required: %s', request.path,
extra={
'status_code': status.HTTP_428_PRECONDITION_REQUIRED,
'request': request
}
)
# raise an RFC 6585 compliant exception
raise PreconditionRequiredException(detail='Precondition required. This "%s" request '
'is required to be conditional. '
'Try again using "%s".' % (
request.method, header)
)
return True
etag = ETAGProcessor
api_etag = APIETAGProcessor
drf-extensions-0.7.1/rest_framework_extensions/etag/mixins.py 0000664 0000000 0000000 00000005447 14100726230 0024606 0 ustar 00root root 0000000 0000000 from rest_framework_extensions.etag.decorators import etag, api_etag
from rest_framework_extensions.settings import extensions_api_settings
class BaseETAGMixin:
# todo: test me. Create generic test like test_etag(view_instance,
# method, should_rebuild_after_method_evaluation)
object_etag_func = extensions_api_settings.DEFAULT_OBJECT_ETAG_FUNC
list_etag_func = extensions_api_settings.DEFAULT_LIST_ETAG_FUNC
class ListETAGMixin(BaseETAGMixin):
@etag(etag_func='list_etag_func')
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
class RetrieveETAGMixin(BaseETAGMixin):
@etag(etag_func='object_etag_func')
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
class UpdateETAGMixin(BaseETAGMixin):
@etag(etag_func='object_etag_func', rebuild_after_method_evaluation=True)
def update(self, request, *args, **kwargs):
return super().update(request, *args, **kwargs)
class DestroyETAGMixin(BaseETAGMixin):
@etag(etag_func='object_etag_func')
def destroy(self, request, *args, **kwargs):
return super().destroy(request, *args, **kwargs)
class ReadOnlyETAGMixin(RetrieveETAGMixin,
ListETAGMixin):
pass
class ETAGMixin(RetrieveETAGMixin,
UpdateETAGMixin,
DestroyETAGMixin,
ListETAGMixin):
pass
class APIBaseETAGMixin:
# todo: test me. Create generic test like test_etag(view_instance,
# method, should_rebuild_after_method_evaluation)
api_object_etag_func = extensions_api_settings.DEFAULT_API_OBJECT_ETAG_FUNC
api_list_etag_func = extensions_api_settings.DEFAULT_API_LIST_ETAG_FUNC
class APIListETAGMixin(APIBaseETAGMixin):
@api_etag(etag_func='api_list_etag_func')
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
class APIRetrieveETAGMixin(APIBaseETAGMixin):
@api_etag(etag_func='api_object_etag_func')
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
class APIUpdateETAGMixin(APIBaseETAGMixin):
@api_etag(etag_func='api_object_etag_func', rebuild_after_method_evaluation=True)
def update(self, request, *args, **kwargs):
return super().update(request, *args, **kwargs)
class APIDestroyETAGMixin(APIBaseETAGMixin):
@api_etag(etag_func='api_object_etag_func')
def destroy(self, request, *args, **kwargs):
return super().destroy(request, *args, **kwargs)
class APIReadOnlyETAGMixin(APIRetrieveETAGMixin,
APIListETAGMixin):
pass
class APIETAGMixin(APIRetrieveETAGMixin,
APIUpdateETAGMixin,
APIDestroyETAGMixin,
APIListETAGMixin):
pass
drf-extensions-0.7.1/rest_framework_extensions/exceptions.py 0000664 0000000 0000000 00000000565 14100726230 0024534 0 ustar 00root root 0000000 0000000 from django.utils.translation import gettext_lazy as _
from rest_framework import status
from rest_framework.exceptions import APIException
class PreconditionRequiredException(APIException):
status_code = status.HTTP_428_PRECONDITION_REQUIRED
default_detail = _('This "{method}" request is required to be conditional.')
default_code = 'precondition_required'
drf-extensions-0.7.1/rest_framework_extensions/fields.py 0000664 0000000 0000000 00000001352 14100726230 0023614 0 ustar 00root root 0000000 0000000 from rest_framework.relations import HyperlinkedRelatedField
class ResourceUriField(HyperlinkedRelatedField):
"""
Represents a hyperlinking uri that points to the
detail view for that object.
Example:
class SurveySerializer(serializers.ModelSerializer):
resource_uri = ResourceUriField(view_name='survey-detail')
class Meta:
model = Survey
fields = ('id', 'resource_uri')
...
{
"id": 1,
"resource_uri": "http://localhost/v1/surveys/1/",
}
"""
# todo: test me
read_only = True
def __init__(self, *args, **kwargs):
kwargs.setdefault('source', '*')
super().__init__(*args, **kwargs)
drf-extensions-0.7.1/rest_framework_extensions/key_constructor/ 0000775 0000000 0000000 00000000000 14100726230 0025230 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/rest_framework_extensions/key_constructor/__init__.py 0000664 0000000 0000000 00000000000 14100726230 0027327 0 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/rest_framework_extensions/key_constructor/bits.py 0000664 0000000 0000000 00000020205 14100726230 0026542 0 ustar 00root root 0000000 0000000 from django.utils.translation import get_language
from django.db.models.query import EmptyQuerySet
from django.core.exceptions import EmptyResultSet
from django.utils.encoding import force_str
from rest_framework_extensions import compat
class AllArgsMixin:
def __init__(self, params='*'):
super().__init__(params)
class KeyBitBase:
def __init__(self, params=None):
self.params = params
def get_data(self, params, view_instance, view_method, request, args, kwargs):
"""
@rtype: dict
"""
raise NotImplementedError()
class KeyBitDictBase(KeyBitBase):
"""Base class for dict-like source data processing.
Look at HeadersKeyBit and QueryParamsKeyBit
"""
def get_data(self, params, view_instance, view_method, request, args, kwargs):
data = {}
if params is not None:
source_dict = self.get_source_dict(
params=params,
view_instance=view_instance,
view_method=view_method,
request=request,
args=args,
kwargs=kwargs
)
if params == '*':
params = source_dict.keys()
for key in params:
value = source_dict.get(
self.prepare_key_for_value_retrieving(key))
if value is not None:
data[self.prepare_key_for_value_assignment(
key)] = force_str(value)
return data
def get_source_dict(self, params, view_instance, view_method, request, args, kwargs):
raise NotImplementedError()
def prepare_key_for_value_retrieving(self, key):
return key
def prepare_key_for_value_assignment(self, key):
return key
class UniqueViewIdKeyBit(KeyBitBase):
def get_data(self, params, view_instance, view_method, request, args, kwargs):
return '.'.join([
view_instance.__module__,
view_instance.__class__.__name__
])
class UniqueMethodIdKeyBit(KeyBitBase):
def get_data(self, params, view_instance, view_method, request, args, kwargs):
return '.'.join([
view_instance.__module__,
view_instance.__class__.__name__,
view_method.__name__
])
class LanguageKeyBit(KeyBitBase):
"""
Return example:
'en'
"""
def get_data(self, params, view_instance, view_method, request, args, kwargs):
return force_str(get_language())
class FormatKeyBit(KeyBitBase):
"""
Return example for json:
u'json'
Return example for html:
u'html'
"""
def get_data(self, params, view_instance, view_method, request, args, kwargs):
return force_str(request.accepted_renderer.format)
class UserKeyBit(KeyBitBase):
"""
Return example for anonymous:
u'anonymous'
Return example for authenticated (value is user id):
u'10'
"""
def get_data(self, params, view_instance, view_method, request, args, kwargs):
if hasattr(request, 'user') and request.user and request.user.is_authenticated:
return force_str(self._get_id_from_user(request.user))
else:
return 'anonymous'
def _get_id_from_user(self, user):
return user.id
class HeadersKeyBit(KeyBitDictBase):
"""
Return example:
{'accept-language': u'ru', 'x-geobase-id': '123'}
"""
def get_source_dict(self, params, view_instance, view_method, request, args, kwargs):
return request.META
def prepare_key_for_value_retrieving(self, key):
from rest_framework_extensions.utils import prepare_header_name
# Accept-Language => http_accept_language
return prepare_header_name(key.lower())
def prepare_key_for_value_assignment(self, key):
return key.lower() # Accept-Language => accept-language
class RequestMetaKeyBit(KeyBitDictBase):
"""
Return example:
{'REMOTE_ADDR': u'127.0.0.2', 'REMOTE_HOST': u'yandex.ru'}
"""
def get_source_dict(self, params, view_instance, view_method, request, args, kwargs):
return request.META
class QueryParamsKeyBit(AllArgsMixin, KeyBitDictBase):
"""
Return example:
{'part': 'Londo', 'callback': 'jquery_callback'}
"""
def get_source_dict(self, params, view_instance, view_method, request, args, kwargs):
return request.GET
class PaginationKeyBit(QueryParamsKeyBit):
"""
Return example:
{'page_size': 100, 'page': '1'}
"""
paginator_attrs = [
'page_query_param', 'page_size_query_param',
'limit_query_param', 'offset_query_param',
'cursor_query_param',
]
def get_data(self, **kwargs):
kwargs['params'] = []
paginator = getattr(kwargs['view_instance'], 'paginator', None)
if paginator:
for attr in self.paginator_attrs:
param = getattr(paginator, attr, None)
if param:
kwargs['params'].append(param)
return super().get_data(**kwargs)
class SqlQueryKeyBitBase(KeyBitBase):
def _get_queryset_query_string(self, queryset):
if isinstance(queryset, EmptyQuerySet):
return None
else:
try:
return force_str(queryset.query.__str__())
except EmptyResultSet:
return None
class ModelInstanceKeyBitBase(KeyBitBase):
"""
Return the actual contents of the query set.
This class is similar to the `SqlQueryKeyBitBase`.
"""
def _get_queryset_query_values(self, queryset):
if isinstance(queryset, EmptyQuerySet) or queryset.count() == 0:
return None
else:
try:
# run through the instances and collect all values in ordered fashion
return compat.queryset_to_value_list(force_str(queryset.values_list()))
except EmptyResultSet:
return None
class ListSqlQueryKeyBit(SqlQueryKeyBitBase):
def get_data(self, params, view_instance, view_method, request, args, kwargs):
queryset = view_instance.filter_queryset(view_instance.get_queryset())
return self._get_queryset_query_string(queryset)
class RetrieveSqlQueryKeyBit(SqlQueryKeyBitBase):
def get_data(self, params, view_instance, view_method, request, args, kwargs):
lookup_value = view_instance.kwargs[view_instance.lookup_field]
try:
queryset = view_instance.filter_queryset(view_instance.get_queryset()).filter(
**{view_instance.lookup_field: lookup_value}
)
except ValueError:
return None
else:
return self._get_queryset_query_string(queryset)
class RetrieveModelKeyBit(ModelInstanceKeyBitBase):
"""
A key bit reflecting the contents of the model instance.
Return example:
u"[(3, False)]"
"""
def get_data(self, params, view_instance, view_method, request, args, kwargs):
lookup_value = view_instance.kwargs[view_instance.lookup_field]
try:
queryset = view_instance.filter_queryset(view_instance.get_queryset()).filter(
**{view_instance.lookup_field: lookup_value}
)
except ValueError:
return None
else:
return self._get_queryset_query_values(queryset)
class ListModelKeyBit(ModelInstanceKeyBitBase):
"""
A key bit reflecting the contents of a list of model instances.
Return example:
u"[(1, True), (2, True), (3, False)]"
"""
def get_data(self, params, view_instance, view_method, request, args, kwargs):
queryset = view_instance.filter_queryset(view_instance.get_queryset())
return self._get_queryset_query_values(queryset)
class ArgsKeyBit(AllArgsMixin, KeyBitBase):
def get_data(self, params, view_instance, view_method, request, args, kwargs):
if params == '*':
return args
elif params is not None:
return [args[i] for i in params]
else:
return []
class KwargsKeyBit(AllArgsMixin, KeyBitDictBase):
def get_source_dict(self, params, view_instance, view_method, request, args, kwargs):
return kwargs
drf-extensions-0.7.1/rest_framework_extensions/key_constructor/constructors.py 0000664 0000000 0000000 00000010156 14100726230 0030355 0 ustar 00root root 0000000 0000000 import hashlib
import json
from rest_framework_extensions.key_constructor import bits
from rest_framework_extensions.settings import extensions_api_settings
class KeyConstructor:
def __init__(self, memoize_for_request=None, params=None):
if memoize_for_request is None:
self.memoize_for_request = extensions_api_settings.DEFAULT_KEY_CONSTRUCTOR_MEMOIZE_FOR_REQUEST
else:
self.memoize_for_request = memoize_for_request
if params is None:
self.params = {}
else:
self.params = params
self.bits = self.get_bits()
def get_bits(self):
_bits = {}
for attr in dir(self.__class__):
attr_value = getattr(self.__class__, attr)
if isinstance(attr_value, bits.KeyBitBase):
_bits[attr] = attr_value
return _bits
def __call__(self, **kwargs):
return self.get_key(**kwargs)
def get_key(self, view_instance, view_method, request, args, kwargs):
if self.memoize_for_request:
memoization_key = self._get_memoization_key(
view_instance=view_instance,
view_method=view_method,
args=args,
kwargs=kwargs
)
if not hasattr(request, '_key_constructor_cache'):
request._key_constructor_cache = {}
if self.memoize_for_request and memoization_key in request._key_constructor_cache:
return request._key_constructor_cache.get(memoization_key)
else:
value = self._get_key(
view_instance=view_instance,
view_method=view_method,
request=request,
args=args,
kwargs=kwargs
)
if self.memoize_for_request:
request._key_constructor_cache[memoization_key] = value
return value
def _get_memoization_key(self, view_instance, view_method, args, kwargs):
from rest_framework_extensions.utils import get_unique_method_id
return json.dumps({
'unique_method_id': get_unique_method_id(view_instance=view_instance, view_method=view_method),
'args': args,
'kwargs': kwargs,
'instance_id': id(self)
})
def _get_key(self, view_instance, view_method, request, args, kwargs):
_kwargs = {
'view_instance': view_instance,
'view_method': view_method,
'request': request,
'args': args,
'kwargs': kwargs,
}
return self.prepare_key(
self.get_data_from_bits(**_kwargs)
)
def prepare_key(self, key_dict):
return hashlib.md5(json.dumps(key_dict, sort_keys=True).encode('utf-8')).hexdigest()
def get_data_from_bits(self, **kwargs):
result_dict = {}
for bit_name, bit_instance in self.bits.items():
if bit_name in self.params:
params = self.params[bit_name]
else:
try:
params = bit_instance.params
except AttributeError:
params = None
result_dict[bit_name] = bit_instance.get_data(
params=params, **kwargs)
return result_dict
class DefaultKeyConstructor(KeyConstructor):
unique_method_id = bits.UniqueMethodIdKeyBit()
format = bits.FormatKeyBit()
language = bits.LanguageKeyBit()
class DefaultObjectKeyConstructor(DefaultKeyConstructor):
retrieve_sql_query = bits.RetrieveSqlQueryKeyBit()
class DefaultListKeyConstructor(DefaultKeyConstructor):
list_sql_query = bits.ListSqlQueryKeyBit()
pagination = bits.PaginationKeyBit()
class DefaultAPIModelInstanceKeyConstructor(KeyConstructor):
"""
Use this constructor when the values of the model instance are required
to identify the resource.
"""
retrieve_model_values = bits.RetrieveModelKeyBit()
class DefaultAPIModelListKeyConstructor(KeyConstructor):
"""
Use this constructor when the values of the model instance are required
to identify many resources.
"""
list_model_values = bits.ListModelKeyBit()
drf-extensions-0.7.1/rest_framework_extensions/mixins.py 0000664 0000000 0000000 00000005460 14100726230 0023661 0 ustar 00root root 0000000 0000000 from rest_framework_extensions.cache.mixins import CacheResponseMixin
# from rest_framework_extensions.etag.mixins import ReadOnlyETAGMixin, ETAGMixin
from rest_framework_extensions.bulk_operations.mixins import ListUpdateModelMixin, ListDestroyModelMixin
from rest_framework_extensions.settings import extensions_api_settings
from django.http import Http404
class DetailSerializerMixin:
"""
Add custom serializer for detail view
"""
serializer_detail_class = None
queryset_detail = None
def get_serializer_class(self):
error_message = "'{0}' should include a 'serializer_detail_class' attribute".format(
self.__class__.__name__)
assert self.serializer_detail_class is not None, error_message
if self._is_request_to_detail_endpoint():
return self.serializer_detail_class
else:
return super().get_serializer_class()
def get_queryset(self, *args, **kwargs):
if self._is_request_to_detail_endpoint() and self.queryset_detail is not None:
return self.queryset_detail.all() # todo: test all()
else:
return super().get_queryset(*args, **kwargs)
def _is_request_to_detail_endpoint(self):
if hasattr(self, 'lookup_url_kwarg'):
lookup = self.lookup_url_kwarg or self.lookup_field
return lookup and lookup in self.kwargs
class PaginateByMaxMixin:
def get_page_size(self, request):
if self.page_size_query_param and self.max_page_size and request.query_params.get(self.page_size_query_param) == 'max':
return self.max_page_size
return super().get_page_size(request)
# class ReadOnlyCacheResponseAndETAGMixin(ReadOnlyETAGMixin, CacheResponseMixin):
# pass
# class CacheResponseAndETAGMixin(ETAGMixin, CacheResponseMixin):
# pass
class NestedViewSetMixin:
def get_queryset(self):
return self.filter_queryset_by_parents_lookups(
super().get_queryset()
)
def filter_queryset_by_parents_lookups(self, queryset):
parents_query_dict = self.get_parents_query_dict()
if parents_query_dict:
try:
return queryset.filter(**parents_query_dict)
except ValueError:
raise Http404
else:
return queryset
def get_parents_query_dict(self):
result = {}
for kwarg_name, kwarg_value in self.kwargs.items():
if kwarg_name.startswith(extensions_api_settings.DEFAULT_PARENT_LOOKUP_KWARG_NAME_PREFIX):
query_lookup = kwarg_name.replace(
extensions_api_settings.DEFAULT_PARENT_LOOKUP_KWARG_NAME_PREFIX,
'',
1
)
query_value = kwarg_value
result[query_lookup] = query_value
return result
drf-extensions-0.7.1/rest_framework_extensions/permissions.py 0000664 0000000 0000000 00000001362 14100726230 0024722 0 ustar 00root root 0000000 0000000 from rest_framework.permissions import DjangoObjectPermissions
class ExtendedDjangoObjectPermissions(DjangoObjectPermissions):
hide_forbidden_for_read_objects = True
def has_object_permission(self, request, view, obj):
if self.hide_forbidden_for_read_objects:
return super().has_object_permission(request, view, obj)
else:
model_cls = getattr(view, 'model', None)
queryset = getattr(view, 'queryset', None)
if model_cls is None and queryset is not None:
model_cls = queryset.model
perms = self.get_required_object_permissions(
request.method, model_cls)
user = request.user
return user.has_perms(perms, obj)
drf-extensions-0.7.1/rest_framework_extensions/routers.py 0000664 0000000 0000000 00000004643 14100726230 0024057 0 ustar 00root root 0000000 0000000 from rest_framework.routers import DefaultRouter, SimpleRouter
from rest_framework_extensions.utils import compose_parent_pk_kwarg_name
class NestedRegistryItem:
def __init__(self, router, parent_prefix, parent_item=None, parent_viewset=None):
self.router = router
self.parent_prefix = parent_prefix
self.parent_item = parent_item
self.parent_viewset = parent_viewset
def register(self, prefix, viewset, basename, parents_query_lookups):
self.router._register(
prefix=self.get_prefix(
current_prefix=prefix,
parents_query_lookups=parents_query_lookups),
viewset=viewset,
basename=basename,
)
return NestedRegistryItem(
router=self.router,
parent_prefix=prefix,
parent_item=self,
parent_viewset=viewset
)
def get_prefix(self, current_prefix, parents_query_lookups):
return '{0}/{1}'.format(
self.get_parent_prefix(parents_query_lookups),
current_prefix
)
def get_parent_prefix(self, parents_query_lookups):
prefix = '/'
current_item = self
i = len(parents_query_lookups) - 1
while current_item:
parent_lookup_value_regex = getattr(
current_item.parent_viewset, 'lookup_value_regex', '[^/.]+')
prefix = '{parent_prefix}/(?P<{parent_pk_kwarg_name}>{parent_lookup_value_regex})/{prefix}'.format(
parent_prefix=current_item.parent_prefix,
parent_pk_kwarg_name=compose_parent_pk_kwarg_name(
parents_query_lookups[i]),
parent_lookup_value_regex=parent_lookup_value_regex,
prefix=prefix
)
i -= 1
current_item = current_item.parent_item
return prefix.strip('/')
class NestedRouterMixin:
def _register(self, *args, **kwargs):
return super().register(*args, **kwargs)
def register(self, *args, **kwargs):
self._register(*args, **kwargs)
return NestedRegistryItem(
router=self,
parent_prefix=self.registry[-1][0],
parent_viewset=self.registry[-1][1]
)
class ExtendedRouterMixin(NestedRouterMixin):
pass
class ExtendedSimpleRouter(ExtendedRouterMixin, SimpleRouter):
pass
class ExtendedDefaultRouter(ExtendedRouterMixin, DefaultRouter):
pass
drf-extensions-0.7.1/rest_framework_extensions/serializers.py 0000664 0000000 0000000 00000004115 14100726230 0024702 0 ustar 00root root 0000000 0000000 from rest_framework_extensions.utils import get_model_opts_concrete_fields
def get_fields_for_partial_update(opts, init_data, fields, init_files=None):
opts = opts.model._meta.concrete_model._meta
partial_fields = list((init_data or {}).keys()) + \
list((init_files or {}).keys())
concrete_field_names = []
for field in get_model_opts_concrete_fields(opts):
if not field.primary_key:
concrete_field_names.append(field.name)
if field.name != field.attname:
concrete_field_names.append(field.attname)
update_fields = []
for field_name in partial_fields:
if field_name in fields:
model_field_name = getattr(
fields[field_name], 'source') or field_name
if model_field_name in concrete_field_names:
update_fields.append(model_field_name)
# recurse on nested fields of same ('*') instance
for k, v in (init_data or {}).items():
if isinstance(v, dict) and k in fields and fields[k].source == '*':
recursive_fields = get_fields_for_partial_update(
opts, v, fields[k].fields.fields)
update_fields.extend(recursive_fields)
return sorted(set(update_fields))
class PartialUpdateSerializerMixin:
def save(self, **kwargs):
self._update_fields = kwargs.get('update_fields', None)
return super().save(**kwargs)
def update(self, instance, validated_attrs):
for attr, value in validated_attrs.items():
if hasattr(getattr(instance, attr, None), 'set'):
getattr(instance, attr).set(value)
else:
setattr(instance, attr, value)
if self.partial and isinstance(instance, self.Meta.model):
instance.save(
update_fields=getattr(self, '_update_fields') or get_fields_for_partial_update(
opts=self.Meta,
init_data=self.get_initial(),
fields=self.fields.fields
)
)
else:
instance.save()
return instance
drf-extensions-0.7.1/rest_framework_extensions/settings.py 0000664 0000000 0000000 00000003155 14100726230 0024211 0 ustar 00root root 0000000 0000000 from django.conf import settings
from rest_framework.settings import APISettings
USER_SETTINGS = getattr(settings, 'REST_FRAMEWORK_EXTENSIONS', None)
DEFAULTS = {
# caching
'DEFAULT_USE_CACHE': 'default',
'DEFAULT_CACHE_RESPONSE_TIMEOUT': None,
'DEFAULT_CACHE_ERRORS': True,
'DEFAULT_CACHE_KEY_FUNC': 'rest_framework_extensions.utils.default_cache_key_func',
'DEFAULT_OBJECT_CACHE_KEY_FUNC': 'rest_framework_extensions.utils.default_object_cache_key_func',
'DEFAULT_LIST_CACHE_KEY_FUNC': 'rest_framework_extensions.utils.default_list_cache_key_func',
# ETAG
'DEFAULT_ETAG_FUNC': 'rest_framework_extensions.utils.default_etag_func',
'DEFAULT_OBJECT_ETAG_FUNC': 'rest_framework_extensions.utils.default_object_etag_func',
'DEFAULT_LIST_ETAG_FUNC': 'rest_framework_extensions.utils.default_list_etag_func',
# API - ETAG
'DEFAULT_API_OBJECT_ETAG_FUNC': 'rest_framework_extensions.utils.default_api_object_etag_func',
'DEFAULT_API_LIST_ETAG_FUNC': 'rest_framework_extensions.utils.default_api_list_etag_func',
# other
'DEFAULT_KEY_CONSTRUCTOR_MEMOIZE_FOR_REQUEST': False,
'DEFAULT_BULK_OPERATION_HEADER_NAME': 'X-BULK-OPERATION',
'DEFAULT_PARENT_LOOKUP_KWARG_NAME_PREFIX': 'parent_lookup_'
}
IMPORT_STRINGS = [
'DEFAULT_CACHE_KEY_FUNC',
'DEFAULT_OBJECT_CACHE_KEY_FUNC',
'DEFAULT_LIST_CACHE_KEY_FUNC',
'DEFAULT_ETAG_FUNC',
'DEFAULT_OBJECT_ETAG_FUNC',
'DEFAULT_LIST_ETAG_FUNC',
# API - ETAG
'DEFAULT_API_OBJECT_ETAG_FUNC',
'DEFAULT_API_LIST_ETAG_FUNC',
]
extensions_api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)
drf-extensions-0.7.1/rest_framework_extensions/test.py 0000664 0000000 0000000 00000001071 14100726230 0023323 0 ustar 00root root 0000000 0000000 # Leaving this module here for backwards compatibility but this is just proxy
# for rest_framework.test
import warnings
from rest_framework.test import (
force_authenticate,
APIRequestFactory,
ForceAuthClientHandler,
APIClient,
APITransactionTestCase,
APITestCase
)
__all__ = (
'force_authenticate,'
'APIRequestFactory,'
'ForceAuthClientHandler,'
'APIClient,'
'APITransactionTestCase,'
'APITestCase'
)
warnings.warn(
'Use of this module is deprecated! Use rest_framework.test instead.',
DeprecationWarning
)
drf-extensions-0.7.1/rest_framework_extensions/utils.py 0000664 0000000 0000000 00000003715 14100726230 0023513 0 ustar 00root root 0000000 0000000 import itertools
from distutils.version import LooseVersion
import rest_framework
from rest_framework_extensions.key_constructor.constructors import (
DefaultKeyConstructor,
DefaultObjectKeyConstructor,
DefaultListKeyConstructor,
DefaultAPIModelInstanceKeyConstructor,
DefaultAPIModelListKeyConstructor
)
from rest_framework_extensions.settings import extensions_api_settings
def get_rest_framework_version():
return tuple(LooseVersion(rest_framework.VERSION).version)
def flatten(list_of_lists):
"""
Takes an iterable of iterables,
returns a single iterable containing all items
"""
# todo: test me
return itertools.chain(*list_of_lists)
def prepare_header_name(name):
"""
>> prepare_header_name('Accept-Language')
http_accept_language
"""
return 'http_{0}'.format(name.strip().replace('-', '_')).upper()
def get_unique_method_id(view_instance, view_method):
# todo: test me as UniqueMethodIdKeyBit
return '.'.join([
view_instance.__module__,
view_instance.__class__.__name__,
view_method.__name__
])
def get_model_opts_concrete_fields(opts):
# todo: test me
if not hasattr(opts, 'concrete_fields'):
opts.concrete_fields = [f for f in opts.fields if f.column is not None]
return opts.concrete_fields
def compose_parent_pk_kwarg_name(value):
return '{0}{1}'.format(
extensions_api_settings.DEFAULT_PARENT_LOOKUP_KWARG_NAME_PREFIX,
value
)
default_cache_key_func = DefaultKeyConstructor()
default_object_cache_key_func = DefaultObjectKeyConstructor()
default_list_cache_key_func = DefaultListKeyConstructor()
default_etag_func = default_cache_key_func
default_object_etag_func = default_object_cache_key_func
default_list_etag_func = default_list_cache_key_func
# API (object-centered) functions
default_api_object_etag_func = DefaultAPIModelInstanceKeyConstructor()
default_api_list_etag_func = DefaultAPIModelListKeyConstructor()
drf-extensions-0.7.1/setup.cfg 0000664 0000000 0000000 00000000034 14100726230 0016300 0 ustar 00root root 0000000 0000000 [bdist_wheel]
universal = 1
drf-extensions-0.7.1/setup.py 0000664 0000000 0000000 00000005007 14100726230 0016176 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
from setuptools import setup
import re
import os
import sys
def get_version(package):
"""
Return package version as listed in `__version__` in `init.py`.
"""
init_py = open(os.path.join(package, '__init__.py')).read()
return re.match("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1)
def get_packages(package):
"""
Return root package and all sub-packages.
"""
return [dirpath
for dirpath, dirnames, filenames in os.walk(package)
if os.path.exists(os.path.join(dirpath, '__init__.py'))]
def get_package_data(package):
"""
Return all files under the root package, that are not in a
package themselves.
"""
walk = [(dirpath.replace(package + os.sep, '', 1), filenames)
for dirpath, dirnames, filenames in os.walk(package)
if not os.path.exists(os.path.join(dirpath, '__init__.py'))]
filepaths = []
for base, filenames in walk:
filepaths.extend([os.path.join(base, filename)
for filename in filenames])
return {package: filepaths}
version = get_version('rest_framework_extensions')
if sys.argv[-1] == 'publish':
os.system("python setup.py sdist upload")
os.system("python setup.py bdist_wheel upload")
print("You probably want to also tag the version now:")
print(" git tag -a %s -m 'version %s'" % (version, version))
print(" git push --tags")
sys.exit()
setup(
name='drf-extensions',
version=version,
url='http://github.com/chibisov/drf-extensions',
download_url='https://pypi.python.org/pypi/drf-extensions/',
license='BSD',
install_requires=['djangorestframework>=3.9.3'],
description='Extensions for Django REST Framework',
long_description='DRF-extensions is a collection of custom extensions for Django REST Framework',
author='Asif Saif Uddin, Gennady Chibisov',
author_email='auvipy@gmail.com',
packages=get_packages('rest_framework_extensions'),
package_data=get_package_data('rest_framework_extensions'),
test_suite='rest_framework_extensions.runtests.runtests.main',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Web Environment',
'Framework :: Django',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Topic :: Internet :: WWW/HTTP',
]
)
drf-extensions-0.7.1/tests_app/ 0000775 0000000 0000000 00000000000 14100726230 0016464 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/__init__.py 0000664 0000000 0000000 00000000000 14100726230 0020563 0 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/plugins.py 0000664 0000000 0000000 00000004032 14100726230 0020516 0 ustar 00root root 0000000 0000000 import shutil
import os
from django_nose.plugin import AlwaysOnPlugin
from django.test import TestCase
from django.core.cache import cache
from django.conf import settings
class UnitTestDiscoveryPlugin(AlwaysOnPlugin):
"""
Enables unittest compatibility mode (dont test functions, only TestCase
subclasses, and only methods that start with [Tt]est).
"""
enabled = True
def wantModule(self, module):
return True
def wantFile(self, file):
if file.endswith('.py'):
return True
def wantClass(self, cls):
if not issubclass(cls, TestCase):
return False
def wantMethod(self, method):
if not method.__name__.lower().startswith('test'):
return False
def wantFunction(self, function):
return False
class PrepareRestFrameworkSettingsPlugin(AlwaysOnPlugin):
def begin(self):
self._monkeypatch_default_settings()
def _monkeypatch_default_settings(self):
from rest_framework import settings
PATCH_REST_FRAMEWORK = {
# Testing
'TEST_REQUEST_RENDERER_CLASSES': (
'rest_framework.renderers.MultiPartRenderer',
'rest_framework.renderers.JSONRenderer'
),
'TEST_REQUEST_DEFAULT_FORMAT': 'multipart',
}
for key, value in PATCH_REST_FRAMEWORK.items():
if key not in settings.DEFAULTS:
settings.DEFAULTS[key] = value
class PrepareFileStorageDir(AlwaysOnPlugin):
def begin(self):
if not os.path.isdir(settings.MEDIA_ROOT):
os.makedirs(settings.MEDIA_ROOT)
def finalize(self, result):
shutil.rmtree(settings.MEDIA_ROOT, ignore_errors=True)
class FlushCache(AlwaysOnPlugin):
# startTest didn't work :(
def begin(self):
self._monkeypatch_testcase()
def _monkeypatch_testcase(self):
old_run = TestCase.run
def new_run(*args, **kwargs):
cache.clear()
return old_run(*args, **kwargs)
TestCase.run = new_run
drf-extensions-0.7.1/tests_app/requirements.txt 0000664 0000000 0000000 00000000057 14100726230 0021752 0 ustar 00root root 0000000 0000000 nose
django-nose
django-filter>=2.1.0
mock
ipdb drf-extensions-0.7.1/tests_app/settings.py 0000664 0000000 0000000 00000011054 14100726230 0020677 0 ustar 00root root 0000000 0000000 # Django settings for testproject project.
import multiprocessing
DEBUG = True
DEBUG_PROPAGATE_EXCEPTIONS = True
ALLOWED_HOSTS = ['*']
ADMINS = (
# ('Your Name', 'your_email@domain.com'),
)
MANAGERS = ADMINS
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'drf_extensions',
'TEST_CHARSET': 'utf8',
},
}
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
},
'special_cache': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
},
'another_special_cache': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
},
}
# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
# On Unix systems, a value of None will cause Django to use the same
# timezone as the operating system.
# If running in a Windows environment this must be set to the same as your
# system time zone.
TIME_ZONE = 'Europe/London'
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = 'en-uk'
SITE_ID = 1
# If you set this to False, Django will make some optimizations so as not
# to load the internationalization machinery.
USE_I18N = True
# If you set this to False, Django will not format dates, numbers and
# calendars according to the current locale
USE_L10N = True
# Absolute filesystem path to the directory that will hold user-uploaded files.
# Example: "/home/media/media.lawrence.com/"
MEDIA_ROOT = 'tests_app/tests/files'
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash if there is a path component (optional in other cases).
# Examples: "http://media.lawrence.com", "http://example.com/media/"
MEDIA_URL = ''
# Make this unique, and don't share it with anybody.
SECRET_KEY = 'u@x-aj9(hoh#rb-^ymf#g2jx_hp0vj7u5#b@ag1n^seu9e!%cy'
# List of callables that know how to import templates from various sources.
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'urls'
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
# Uncomment the next line to enable the admin:
# 'django.contrib.admin',
# Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs',
'django_nose',
'guardian',
'rest_framework_extensions',
'tests_app.tests.functional',
'tests_app.tests.unit',
)
STATIC_URL = '/static/'
# Password validation
# https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
AUTH_USER_MODEL = 'auth.User'
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
NOSE_ARGS = [
'--processes=%s' % multiprocessing.cpu_count(),
'--process-timeout=100',
'--nocapture',
]
NOSE_PLUGINS = [
'plugins.UnitTestDiscoveryPlugin',
'plugins.PrepareRestFrameworkSettingsPlugin',
'plugins.FlushCache',
'plugins.PrepareFileStorageDir'
]
# guardian
ANONYMOUS_USER_ID = -1
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', # this is default
'guardian.backends.ObjectPermissionBackend',
)
drf-extensions-0.7.1/tests_app/tests/ 0000775 0000000 0000000 00000000000 14100726230 0017626 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/__init__.py 0000664 0000000 0000000 00000000000 14100726230 0021725 0 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/ 0000775 0000000 0000000 00000000000 14100726230 0021770 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/__init__.py 0000664 0000000 0000000 00000000000 14100726230 0024067 0 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/_concurrency/ 0000775 0000000 0000000 00000000000 14100726230 0024461 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/_concurrency/__init__.py 0000664 0000000 0000000 00000000000 14100726230 0026560 0 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/_concurrency/conditional_request/ 0000775 0000000 0000000 00000000000 14100726230 0030534 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/_concurrency/conditional_request/__init__.py 0000664 0000000 0000000 00000000000 14100726230 0032633 0 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/_concurrency/conditional_request/models.py 0000664 0000000 0000000 00000000542 14100726230 0032372 0 ustar 00root root 0000000 0000000 from django.db import models
class Book(models.Model):
"""A sample model for conditional requests."""
name = models.CharField(max_length=100, default=None, blank=True, null=True)
author = models.CharField(max_length=100, default=None, blank=True, null=True)
issn = models.CharField(max_length=100, default=None, blank=True, null=True)
drf-extensions-0.7.1/tests_app/tests/functional/_concurrency/conditional_request/serializers.py 0000664 0000000 0000000 00000000265 14100726230 0033445 0 ustar 00root root 0000000 0000000 from rest_framework import serializers
from .models import Book
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = '__all__'
drf-extensions-0.7.1/tests_app/tests/functional/_concurrency/conditional_request/tests.py 0000664 0000000 0000000 00000044410 14100726230 0032253 0 ustar 00root root 0000000 0000000 from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from .models import Book
from django.test import override_settings
import json
@override_settings(ROOT_URLCONF='tests_app.tests.functional.concurrency.conditional_request.urls')
class BookAPITestCases(APITestCase):
"""
Run the conditional requests test cases.
`tox -- tests_app.tests.functional.concurrency.conditional_request.tests`
"""
def setUp(self):
# create a book
self.book = Book.objects.create(name='The Summons',
author='Stephen King',
issn='9780345531988')
def alter_book_issn(self, issn='0123456789012'):
"""Mimic alteration of object in the DB."""
self.book.issn = issn
self.book.save()
return self.book
def test_book_retrieve_cache_hit(self):
"""Test idempotent retrieve using 'If-None-Match' HTTP header, should result in HTTP 304."""
book_response = self.client.get(reverse('book-detail', kwargs={'pk': self.book.id}),
CONTENT_TYPE='application/json')
self.assertEqual(book_response.status_code, status.HTTP_200_OK)
# memorize the ETag from the response to send with the next request
etag = book_response['ETag']
# issue the same request again
book_response = self.client.get(reverse('book-detail', kwargs={'pk': self.book.id}),
CONTENT_TYPE='application/json',
HTTP_IF_NONE_MATCH=etag)
self.assertEqual(book_response.status_code, status.HTTP_304_NOT_MODIFIED,
'The response status code must be 304!')
self.assertEqual(book_response['ETag'], etag)
def test_book_retrieve_cache_miss(self):
"""Test idempotent retrieve using 'If-None-Match' HTTP header, should result in HTTP 200."""
book_response = self.client.get(reverse('book-detail', kwargs={'pk': self.book.id}),
CONTENT_TYPE='application/json')
self.assertEqual(book_response.status_code, status.HTTP_200_OK)
# memorize the ETag from the response to send with the next request
etag = book_response['ETag']
# simulate background activity on the book
self.alter_book_issn()
# issue the same request again
book_response = self.client.get(reverse('book-detail', kwargs={'pk': self.book.id}),
CONTENT_TYPE='application/json',
HTTP_IF_NONE_MATCH=etag)
self.assertEqual(book_response.status_code, status.HTTP_200_OK,
'The response status code must be 200!')
self.assertNotEqual(book_response['ETag'], etag)
def test_book_update_unconditional(self):
"""Test an update without providing the 'ETag' HTTP header, should yield HTTP 200."""
book_response = self.client.get(reverse('book-detail', kwargs={'pk': self.book.id}),
CONTENT_TYPE='application/json')
self.assertEqual(book_response.status_code, status.HTTP_200_OK)
book_json = json.loads(book_response.content.decode())
# alter the author
book_json['author'] = 'John Grisham'
url = reverse('book_view-unconditional_update', kwargs={'pk': book_json['id']})
response = self.client.put(url,
data=book_json)
self.assertEqual(response.status_code, status.HTTP_200_OK, 'The response status code must be 200!')
updated_book_json = json.loads(response.content.decode())
self.assertEqual(updated_book_json['author'], book_json['author'], 'Author must be John Grisham!')
def test_book_delete_unconditional(self):
"""Test delete, should result in HTTP 204."""
# retrieve
book_response = self.client.get(reverse('book-detail', kwargs={'pk': self.book.id}),
CONTENT_TYPE='application/json')
self.assertEqual(book_response.status_code, status.HTTP_200_OK)
# delete
response = self.client.delete(reverse('book_view-unconditional_delete', kwargs={'pk': self.book.id}))
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, 'The response status code must be 204!')
def test_book_conditional_update(self):
"""Test a conditional update of a book using 'If-Match' HTTP header, should yield HTTP 200."""
book_response = self.client.get(reverse('book-detail', kwargs={'pk': self.book.id}),
CONTENT_TYPE='application/json')
self.assertEqual(book_response.status_code, status.HTTP_200_OK)
self.assertIsNotNone(book_response['ETag'])
book_json = json.loads(book_response.content.decode())
# memorize the ETag from the response to send with the next request
etag = book_response['ETag']
# alter the author
book_json['author'] = 'John Grisham'
url = reverse('book-detail', kwargs={'pk': book_json['id']})
response = self.client.put(url,
data=book_json,
HTTP_IF_MATCH=etag) # <-- set the ETag header to trigger the conditional request
# ######################## ######################## ########################
# this update must succeed, since the if-match header is the same as the ETag sent from the server!
self.assertNotEqual(response.status_code, status.HTTP_412_PRECONDITION_FAILED, 'The response status code must '
'not be 412!')
self.assertEqual(response.status_code, status.HTTP_200_OK, 'The response status code must be 200!')
# ######################## ######################## ########################
updated_book_json = json.loads(response.content.decode())
self.assertEqual(updated_book_json['author'], book_json['author'], 'Author must be John Grisham!')
def test_book_conditional_update_fail(self):
"""Test a conditional update of a book using 'If-Match' HTTP header, should yield HTTP 412."""
book_response = self.client.get(reverse('book-detail', kwargs={'pk': self.book.id}),
CONTENT_TYPE='application/json')
self.assertEqual(book_response.status_code, status.HTTP_200_OK)
self.assertIsNotNone(book_response['ETag'])
book_json = json.loads(book_response.content.decode())
# memorize the ETag from the response to send with the next request
etag = book_response['ETag']
# mimic background activity
self.alter_book_issn()
# alter the author
book_json['author'] = 'John Grisham'
url = reverse('book-detail', kwargs={'pk': book_json['id']})
response = self.client.put(url,
data=book_json,
HTTP_IF_MATCH=etag) # <-- set the ETag header to trigger the conditional request
self.assertEqual(response.status_code, status.HTTP_412_PRECONDITION_FAILED, 'The response status code must '
'be 412!')
def test_book_conditional_update_fail_no_if_match(self):
"""Test a conditional update of a book using no HTTP 'If-Match' header, should yield HTTP 428."""
book_response = self.client.get(reverse('book-detail', kwargs={'pk': self.book.id}),
CONTENT_TYPE='application/json')
self.assertEqual(book_response.status_code, status.HTTP_200_OK)
self.assertIsNotNone(book_response['ETag'])
book_json = json.loads(book_response.content.decode())
# alter the author
book_json['author'] = 'John Grisham'
url = reverse('book-detail', kwargs={'pk': book_json['id']})
response = self.client.put(url,
data=book_json)
self.assertEqual(response.status_code, status.HTTP_428_PRECONDITION_REQUIRED, 'The response status code must '
'be 428!')
def test_book_conditional_update_fail_first_then_succeed(self):
"""Test a conditional update of a book using 'If-Match' HTTP header, should yield HTTP 412, then 200."""
book_response = self.client.get(reverse('book-detail', kwargs={'pk': self.book.id}),
CONTENT_TYPE='application/json')
self.assertEqual(book_response.status_code, status.HTTP_200_OK)
self.assertIsNotNone(book_response['ETag'])
book_json = json.loads(book_response.content.decode())
# memorize the ETag from the response to send with the next request
etag = book_response['ETag']
# mimic background activity
self.alter_book_issn()
# try to alter the author
book_json['author'] = 'John Grisham'
url = reverse('book-detail', kwargs={'pk': book_json['id']})
response = self.client.put(url,
data=book_json,
HTTP_IF_MATCH=etag) # <-- set the ETag header to trigger the conditional request
# ######################## ######################## ########################
# this update must succeed, since the if-match header is the same as the ETag sent from the server!
self.assertEqual(response.status_code, status.HTTP_412_PRECONDITION_FAILED, 'The response status code must '
'be 412!')
# ######################## ######################## ########################
# fetch the instance again and try again
book_response = self.client.get(reverse('book-detail', kwargs={'pk': self.book.id}),
CONTENT_TYPE='application/json')
self.assertEqual(book_response.status_code, status.HTTP_200_OK)
self.assertIsNotNone(book_response['ETag'])
book_json = json.loads(book_response.content.decode())
# memorize the ETag from the response to send with the next request
new_etag = book_response['ETag']
# ...client merges/rejects the local changes...
# try again to update
response = self.client.put(url,
data=book_json,
HTTP_IF_MATCH=new_etag) # <-- set the ETag header to trigger the conditional request
# ######################## ######################## ########################
# this update must succeed, since the if-match header is the same as the ETag sent from the server!
self.assertNotEqual(response.status_code, status.HTTP_412_PRECONDITION_FAILED, 'The response status code must '
'not be 412!')
self.assertEqual(response.status_code, status.HTTP_200_OK, 'The response status code must be 200!')
# ######################## ######################## ########################
updated_book_json = json.loads(response.content.decode())
self.assertEqual(updated_book_json['author'], book_json['author'], 'Author must be John Grisham!')
def test_book_conditional_delete_default_viewset(self):
"""Test conditional delete using 'If-Match' HTTP header, should result in HTTP 204."""
book_response = self.client.get(reverse('book-detail', kwargs={'pk': self.book.id}),
CONTENT_TYPE='application/json')
self.assertEqual(book_response.status_code, status.HTTP_200_OK)
# memorize the ETag from the response to send with the next request
etag = book_response['ETag']
# delete the instance
book_response = self.client.delete(reverse('book-detail', kwargs={'pk': self.book.id}),
HTTP_IF_MATCH=etag)
self.assertEqual(book_response.status_code, status.HTTP_204_NO_CONTENT, 'The response status code must be 204!')
def test_book_conditional_delete_custom_view(self):
"""Test conditional delete using 'If-Match' HTTP header, should result in HTTP 204."""
book_response = self.client.get(reverse('book-detail', kwargs={'pk': self.book.id}),
CONTENT_TYPE='application/json')
self.assertEqual(book_response.status_code, status.HTTP_200_OK)
# memorize the ETag from the response to send with the next request
etag = book_response['ETag']
# delete the instance
book_response = self.client.delete(reverse('book_view-custom_delete', kwargs={'pk': self.book.id}),
HTTP_IF_MATCH=etag)
self.assertEqual(book_response.status_code, status.HTTP_204_NO_CONTENT, 'The response status code must be 204!')
def test_book_conditional_delete_fail(self):
"""Test conditional delete using 'If-Match' HTTP header, should result in HTTP 412."""
book_response = self.client.get(reverse('book-detail', kwargs={'pk': self.book.id}),
CONTENT_TYPE='application/json')
self.assertEqual(book_response.status_code, status.HTTP_200_OK)
# memorize the ETag from the response to send with the next request
etag = book_response['ETag']
# alter the book
self.alter_book_issn()
# delete the instance
book_response = self.client.delete(reverse('book-detail', kwargs={'pk': self.book.id}),
HTTP_IF_MATCH=etag)
self.assertEqual(book_response.status_code, status.HTTP_412_PRECONDITION_FAILED,
'The response status code must be 412!')
def test_book_conditional_delete_fail_no_if_match(self):
"""Test conditional delete without 'If-Match' HTTP header, should result in HTTP 428."""
book_response = self.client.get(reverse('book-detail', kwargs={'pk': self.book.id}),
CONTENT_TYPE='application/json')
self.assertEqual(book_response.status_code, status.HTTP_200_OK)
# delete the instance
book_response = self.client.delete(reverse('book-detail', kwargs={'pk': self.book.id}))
self.assertEqual(book_response.status_code, status.HTTP_428_PRECONDITION_REQUIRED,
'The response status code must be 428!')
def test_book_retrieve_cache_hit_view(self):
"""Test idempotent retrieve using 'If-None-Match' HTTP header, should result in HTTP 304."""
book_response = self.client.get(reverse('book_view-detail', kwargs={'pk': self.book.id}),
CONTENT_TYPE='application/json')
self.assertEqual(book_response.status_code, status.HTTP_200_OK)
# memorize the ETag from the response to send with the next request
etag = book_response['ETag']
# issue the same request again
book_response = self.client.get(reverse('book_view-detail', kwargs={'pk': self.book.id}),
CONTENT_TYPE='application/json',
HTTP_IF_NONE_MATCH=etag)
self.assertEqual(book_response.status_code, status.HTTP_304_NOT_MODIFIED,
'The response status code must be 304!')
self.assertEqual(book_response['ETag'], etag)
def test_book_conditional_custom_delete_decorator(self):
"""Test conditional delete using 'If-Match' HTTP header, should result in HTTP 204."""
book_response = self.client.get(reverse('book-detail', kwargs={'pk': self.book.id}),
CONTENT_TYPE='application/json')
self.assertEqual(book_response.status_code, status.HTTP_200_OK)
# memorize the ETag from the response to send with the next request
etag = book_response['ETag']
# delete the instance
book_response = self.client.delete(reverse('book_view-custom_delete', kwargs={'pk': self.book.id}),
HTTP_IF_MATCH=etag)
self.assertEqual(book_response.status_code, status.HTTP_204_NO_CONTENT,
'The response status code must be 204!')
def test_book_conditional_custom_delete_decorator_fail(self):
"""Test conditional delete using 'If-Match' HTTP header, should result in HTTP 412."""
book_response = self.client.get(reverse('book-detail', kwargs={'pk': self.book.id}),
CONTENT_TYPE='application/json')
self.assertEqual(book_response.status_code, status.HTTP_200_OK)
# memorize the ETag from the response to send with the next request
etag = book_response['ETag']
# change the instance in the DB
self.alter_book_issn()
# delete the instance
book_response = self.client.delete(reverse('book_view-custom_delete', kwargs={'pk': self.book.id}),
HTTP_IF_MATCH=etag)
self.assertEqual(book_response.status_code, status.HTTP_412_PRECONDITION_FAILED,
'The response status code must be 412!')
def test_book_conditional_custom_delete_decorator_fail__not_found(self):
"""Test conditional delete using 'If-Match' HTTP header, should result in HTTP 412."""
book_response = self.client.get(reverse('book-detail', kwargs={'pk': self.book.id}),
CONTENT_TYPE='application/json')
self.assertEqual(book_response.status_code, status.HTTP_200_OK)
book_json = json.loads(book_response.content.decode())
# memorize the ETag from the response to send with the next request
etag = book_response['ETag']
# delete the instance in the DB
self.book.delete()
# delete the instance
book_response = self.client.delete(reverse('book_view-custom_delete',
kwargs={'pk': book_json['id']}),
HTTP_IF_MATCH=etag)
self.assertEqual(book_response.status_code, status.HTTP_412_PRECONDITION_FAILED,
'The response status code must be 412!')
drf-extensions-0.7.1/tests_app/tests/functional/_concurrency/conditional_request/urls.py 0000664 0000000 0000000 00000002044 14100726230 0032073 0 ustar 00root root 0000000 0000000 from django.conf.urls import url, include
from rest_framework import routers
from .views import (BookViewSet, BookListCreateView, BookChangeView, BookCustomDestroyView,
BookUnconditionalDestroyView, BookUnconditionalUpdateView)
router = routers.DefaultRouter()
router.register(r'books', BookViewSet)
urlpatterns = [
# manually add endpoints for APIView instances
url(r'books_view/(?P[0-9]+)/custom/delete/', BookCustomDestroyView.as_view(), name='book_view-custom_delete'),
url(r'books_view/(?P[0-9]+)/unconditional/delete/', BookUnconditionalDestroyView.as_view(),
name='book_view-unconditional_delete'),
url(r'books_view/(?P[0-9]+)/unconditional/update/', BookUnconditionalUpdateView.as_view(),
name='book_view-unconditional_update'),
url(r'books_view/', BookListCreateView.as_view(), name='book_view-list'),
url(r'books_view/(?P[0-9]+)/', BookChangeView.as_view(), name='book_view-detail'),
# include the URLs from the default viewset
url(r'^', include(router.urls)),
]
drf-extensions-0.7.1/tests_app/tests/functional/_concurrency/conditional_request/views.py 0000664 0000000 0000000 00000004601 14100726230 0032244 0 ustar 00root root 0000000 0000000 from rest_framework import viewsets
from rest_framework import generics
from rest_framework import status
from rest_framework.response import Response
from rest_framework_extensions.etag.mixins import APIETAGMixin
from rest_framework_extensions.etag.decorators import api_etag
from rest_framework_extensions.utils import default_api_object_etag_func
from .models import Book
from .serializers import BookSerializer
class BookViewSet(APIETAGMixin,
viewsets.ModelViewSet):
"""Test the mixin with DRF viewset."""
queryset = Book.objects.all()
serializer_class = BookSerializer
class BookChangeView(APIETAGMixin,
generics.RetrieveUpdateDestroyAPIView):
"""Test the mixin with DRF generic API views."""
queryset = Book.objects.all()
serializer_class = BookSerializer
class BookListCreateView(APIETAGMixin,
generics.ListCreateAPIView):
"""Test the mixin with DRF generic API views."""
queryset = Book.objects.all()
serializer_class = BookSerializer
class BookCustomDestroyView(generics.DestroyAPIView):
"""Test the decorator with DRF generic API views."""
# include the queryset here to enable the object lookup in `@api_etag`
queryset = Book.objects.all()
@api_etag(etag_func=default_api_object_etag_func)
def delete(self, request, *args, **kwargs):
obj = Book.objects.get(id=kwargs['pk'])
obj.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class BookUnconditionalDestroyView(generics.DestroyAPIView):
"""Test the decorator with DRF generic API views."""
# include the queryset here to enable the object lookup in `@api_etag`
queryset = Book.objects.all()
@api_etag(etag_func=default_api_object_etag_func, precondition_map={})
def delete(self, request, *args, **kwargs):
obj = Book.objects.get(id=kwargs['pk'])
obj.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class BookUnconditionalUpdateView(generics.UpdateAPIView):
"""Test the decorator with DRF generic API views."""
# include the queryset here to enable the object lookup in `@api_etag`
queryset = Book.objects.all()
serializer_class = BookSerializer
@api_etag(etag_func=default_api_object_etag_func, precondition_map={})
def update(self, request, *args, **kwargs):
return super().update(request, *args, **kwargs) drf-extensions-0.7.1/tests_app/tests/functional/_examples/ 0000775 0000000 0000000 00000000000 14100726230 0023745 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/_examples/__init__.py 0000664 0000000 0000000 00000000001 14100726230 0026045 0 ustar 00root root 0000000 0000000
drf-extensions-0.7.1/tests_app/tests/functional/_examples/etags/ 0000775 0000000 0000000 00000000000 14100726230 0025050 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/_examples/etags/__init__.py 0000664 0000000 0000000 00000000001 14100726230 0027150 0 ustar 00root root 0000000 0000000
drf-extensions-0.7.1/tests_app/tests/functional/_examples/etags/remove_etag_gzip_postfix/ 0000775 0000000 0000000 00000000000 14100726230 0032152 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/_examples/etags/remove_etag_gzip_postfix/__init__.py0000664 0000000 0000000 00000000001 14100726230 0034252 0 ustar 00root root 0000000 0000000
middleware.py 0000664 0000000 0000000 00000000563 14100726230 0034566 0 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/_examples/etags/remove_etag_gzip_postfix try:
from django.utils.deprecation import MiddlewareMixin
except ImportError:
MiddlewareMixin = object
class RemoveEtagGzipPostfix(MiddlewareMixin):
def process_response(self, request, response):
if response.has_header('ETag') and response['ETag'][-6:] == ';gzip"':
response['ETag'] = response['ETag'][:-6] + '"'
return response
drf-extensions-0.7.1/tests_app/tests/functional/_examples/etags/remove_etag_gzip_postfix/tests.py 0000664 0000000 0000000 00000002221 14100726230 0033663 0 ustar 00root root 0000000 0000000 from django.test import TestCase, override_settings
@override_settings(ROOT_URLCONF='tests_app.tests.functional.examples.etags.remove_etag_gzip_postfix.urls')
class RemoveEtagGzipPostfixTest(TestCase):
@override_settings(MIDDLEWARE_CLASSES=(
'django.middleware.gzip.GZipMiddleware',
'django.middleware.common.CommonMiddleware'
))
def test_without_middleware(self):
response = self.client.get('/remove-etag-gzip-postfix/', **{
'HTTP_ACCEPT_ENCODING': 'gzip'
})
self.assertEqual(response.status_code, 200)
self.assertEqual(response['ETag'], '"etag_value;gzip"')
@override_settings(MIDDLEWARE_CLASSES=(
'tests_app.tests.functional.examples.etags.remove_etag_gzip_postfix.middleware.RemoveEtagGzipPostfix',
'django.middleware.gzip.GZipMiddleware',
'django.middleware.common.CommonMiddleware'
))
def test_with_middleware(self):
response = self.client.get('/remove-etag-gzip-postfix/', **{
'HTTP_ACCEPT_ENCODING': 'gzip'
})
self.assertEqual(response.status_code, 200)
self.assertEqual(response['ETag'], '"etag_value"')
drf-extensions-0.7.1/tests_app/tests/functional/_examples/etags/remove_etag_gzip_postfix/urls.py 0000664 0000000 0000000 00000000213 14100726230 0033505 0 ustar 00root root 0000000 0000000 from django.conf.urls import url
from .views import MyView
urlpatterns = [
url(r'^remove-etag-gzip-postfix/$', MyView.as_view()),
]
drf-extensions-0.7.1/tests_app/tests/functional/_examples/etags/remove_etag_gzip_postfix/views.py 0000664 0000000 0000000 00000001062 14100726230 0033660 0 ustar 00root root 0000000 0000000 from django.views import View
from django.http import HttpResponse
class MyView(View):
def get(self, request):
"""
GZipMiddleware will NOT compress content if any of the following are true:
* The content body is less than 200 bytes long.
* The response has already set the Content-Encoding header.
* The request (the browser) hasn’t sent an Accept-Encoding header containing gzip.
"""
response = HttpResponse('r' * 300)
response['ETag'] = '"etag_value"'
return response
drf-extensions-0.7.1/tests_app/tests/functional/cache/ 0000775 0000000 0000000 00000000000 14100726230 0023033 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/cache/__init__.py 0000664 0000000 0000000 00000000001 14100726230 0025133 0 ustar 00root root 0000000 0000000
drf-extensions-0.7.1/tests_app/tests/functional/cache/decorators/ 0000775 0000000 0000000 00000000000 14100726230 0025200 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/cache/decorators/__init__.py 0000664 0000000 0000000 00000000001 14100726230 0027300 0 ustar 00root root 0000000 0000000
drf-extensions-0.7.1/tests_app/tests/functional/cache/decorators/tests.py 0000664 0000000 0000000 00000001112 14100726230 0026707 0 ustar 00root root 0000000 0000000 from django.test import TestCase, override_settings
from django.utils.encoding import force_str
@override_settings(ROOT_URLCONF='tests_app.tests.functional.cache.decorators.urls')
class TestCacheResponseFunctionally(TestCase):
def test_should_return_response(self):
resp = self.client.get('/hello/')
self.assertEqual(force_str(resp.content), '"Hello world"')
def test_should_return_same_response_if_cached(self):
resp_1 = self.client.get('/hello/')
resp_2 = self.client.get('/hello/')
self.assertEqual(resp_1.content, resp_2.content)
drf-extensions-0.7.1/tests_app/tests/functional/cache/decorators/urls.py 0000664 0000000 0000000 00000000214 14100726230 0026534 0 ustar 00root root 0000000 0000000 from django.conf.urls import url
from .views import HelloView
urlpatterns = [
url(r'^hello/$', HelloView.as_view(), name='hello'),
]
drf-extensions-0.7.1/tests_app/tests/functional/cache/decorators/views.py 0000664 0000000 0000000 00000000441 14100726230 0026706 0 ustar 00root root 0000000 0000000 from rest_framework import views
from rest_framework.response import Response
from rest_framework_extensions.cache.decorators import cache_response
class HelloView(views.APIView):
@cache_response()
def get(self, request, *args, **kwargs):
return Response('Hello world')
drf-extensions-0.7.1/tests_app/tests/functional/key_constructor/ 0000775 0000000 0000000 00000000000 14100726230 0025225 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/key_constructor/__init__.py 0000664 0000000 0000000 00000000001 14100726230 0027325 0 ustar 00root root 0000000 0000000
drf-extensions-0.7.1/tests_app/tests/functional/key_constructor/bits/ 0000775 0000000 0000000 00000000000 14100726230 0026166 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/key_constructor/bits/__init__.py 0000664 0000000 0000000 00000000001 14100726230 0030266 0 ustar 00root root 0000000 0000000
drf-extensions-0.7.1/tests_app/tests/functional/key_constructor/bits/models.py 0000664 0000000 0000000 00000000427 14100726230 0030026 0 ustar 00root root 0000000 0000000 from django.db import models
class KeyConstructorUserProperty(models.Model):
name = models.CharField(max_length=100)
class KeyConstructorUserModel(models.Model):
property = models.ForeignKey(
KeyConstructorUserProperty,
on_delete=models.CASCADE
)
drf-extensions-0.7.1/tests_app/tests/functional/key_constructor/bits/serializers.py 0000664 0000000 0000000 00000000341 14100726230 0031072 0 ustar 00root root 0000000 0000000 from rest_framework import serializers
from .models import KeyConstructorUserModel
class UserModelSerializer(serializers.ModelSerializer):
class Meta:
model = KeyConstructorUserModel
fields = '__all__'
drf-extensions-0.7.1/tests_app/tests/functional/key_constructor/bits/tests.py 0000664 0000000 0000000 00000002530 14100726230 0027702 0 ustar 00root root 0000000 0000000 from django.test import override_settings
from rest_framework.test import APITestCase
from .models import KeyConstructorUserProperty
@override_settings(ROOT_URLCONF='tests_app.tests.functional.key_constructor.bits.urls')
class ListSqlQueryKeyBitTestBehaviour(APITestCase):
"""Regression tests for https://github.com/chibisov/drf-extensions/issues/28#issuecomment-51711927
`rest_framework.filters.DjangoFilterBackend` uses defalut `FilterSet`.
When there is no filtered fk in db, then `FilterSet.form` is invalid with errors:
{'property': [u'Select a valid choice. That choice is not one of the available choices.']}
In that case `FilterSet.qs` returns `self.queryset.none()`
"""
def test_with_fk_in_db(self):
KeyConstructorUserProperty.objects.create(name='some property')
# list
response = self.client.get('/users/?property=1')
self.assertEqual(response.status_code, 200)
# retrieve
response = self.client.get('/users/1/?property=1')
self.assertEqual(response.status_code, 404)
def test_without_fk_in_db(self):
# list
response = self.client.get('/users/?property=1')
self.assertEqual(response.status_code, 400)
# retrieve
response = self.client.get('/users/1/?property=1')
self.assertEqual(response.status_code, 400)
drf-extensions-0.7.1/tests_app/tests/functional/key_constructor/bits/urls.py 0000664 0000000 0000000 00000000310 14100726230 0027517 0 ustar 00root root 0000000 0000000 from rest_framework import routers
from .views import UserModelViewSet
viewset_router = routers.DefaultRouter()
viewset_router.register('users', UserModelViewSet)
urlpatterns = viewset_router.urls
drf-extensions-0.7.1/tests_app/tests/functional/key_constructor/bits/views.py 0000664 0000000 0000000 00000000624 14100726230 0027677 0 ustar 00root root 0000000 0000000 import django_filters
from rest_framework import viewsets
from .models import KeyConstructorUserModel as UserModel
from .serializers import UserModelSerializer
class UserModelViewSet(viewsets.ModelViewSet):
queryset = UserModel.objects.all()
serializer_class = UserModelSerializer
filter_backends = (django_filters.rest_framework.DjangoFilterBackend,)
filterset_fields = ('property',)
drf-extensions-0.7.1/tests_app/tests/functional/migrations/ 0000775 0000000 0000000 00000000000 14100726230 0024144 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/migrations/0001_initial.py 0000664 0000000 0000000 00000016345 14100726230 0026620 0 ustar 00root root 0000000 0000000 # Generated by Django 2.2 on 2019-04-16 11:51
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.CreateModel(
name='Comment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(max_length=254)),
('content', models.CharField(max_length=200)),
('created', models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name='CommentForListDestroyModelMixin',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(max_length=254)),
],
),
migrations.CreateModel(
name='CommentForListUpdateModelMixin',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(max_length=254)),
],
),
migrations.CreateModel(
name='CommentForPaginateByMaxMixin',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(max_length=254)),
('content', models.CharField(max_length=200)),
('created', models.DateTimeField(auto_now_add=True)),
],
options={
'ordering': ['id'],
},
),
migrations.CreateModel(
name='DefaultRouterGroupModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=10)),
],
),
migrations.CreateModel(
name='DefaultRouterPermissionModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=10)),
],
),
migrations.CreateModel(
name='KeyConstructorUserProperty',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
],
),
migrations.CreateModel(
name='NestedRouterMixinBookModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=30)),
],
),
migrations.CreateModel(
name='NestedRouterMixinGroupModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=10)),
],
),
migrations.CreateModel(
name='NestedRouterMixinPermissionModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=10)),
],
),
migrations.CreateModel(
name='NestedRouterMixinTaskModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=30)),
],
),
migrations.CreateModel(
name='PermissionsComment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('text', models.CharField(max_length=100)),
],
),
migrations.CreateModel(
name='RouterTestModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.CharField(max_length=20)),
('text', models.CharField(max_length=200)),
],
),
migrations.CreateModel(
name='UserForListUpdateModelMixin',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(max_length=254)),
('name', models.CharField(max_length=10)),
('age', models.IntegerField()),
('last_name', models.CharField(max_length=10)),
('password', models.CharField(max_length=100)),
],
),
migrations.CreateModel(
name='NestedRouterMixinUserModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.EmailField(blank=True, max_length=254, null=True)),
('name', models.CharField(max_length=10)),
('groups', models.ManyToManyField(related_name='user_groups', to='functional.NestedRouterMixinGroupModel')),
],
),
migrations.AddField(
model_name='nestedroutermixingroupmodel',
name='permissions',
field=models.ManyToManyField(to='functional.NestedRouterMixinPermissionModel'),
),
migrations.CreateModel(
name='NestedRouterMixinCommentModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.PositiveIntegerField(blank=True, null=True)),
('text', models.CharField(max_length=30)),
('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
],
),
migrations.CreateModel(
name='KeyConstructorUserModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='functional.KeyConstructorUserProperty')),
],
),
migrations.CreateModel(
name='DefaultRouterUserModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=10)),
('groups', models.ManyToManyField(related_name='user_groups', to='functional.DefaultRouterGroupModel')),
],
),
migrations.AddField(
model_name='defaultroutergroupmodel',
name='permissions',
field=models.ManyToManyField(to='functional.DefaultRouterPermissionModel'),
),
]
drf-extensions-0.7.1/tests_app/tests/functional/migrations/__init__.py 0000664 0000000 0000000 00000000000 14100726230 0026243 0 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/mixins/ 0000775 0000000 0000000 00000000000 14100726230 0023277 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/mixins/__init__.py 0000664 0000000 0000000 00000000000 14100726230 0025376 0 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/mixins/detail_serializer_mixin/ 0000775 0000000 0000000 00000000000 14100726230 0030176 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/mixins/detail_serializer_mixin/__init__.py 0000664 0000000 0000000 00000000001 14100726230 0032276 0 ustar 00root root 0000000 0000000
drf-extensions-0.7.1/tests_app/tests/functional/mixins/detail_serializer_mixin/models.py 0000664 0000000 0000000 00000000301 14100726230 0032025 0 ustar 00root root 0000000 0000000 from django.db import models
class Comment(models.Model):
email = models.EmailField()
content = models.CharField(max_length=200)
created = models.DateTimeField(auto_now_add=True)
drf-extensions-0.7.1/tests_app/tests/functional/mixins/detail_serializer_mixin/serializers.py 0000664 0000000 0000000 00000000650 14100726230 0033105 0 ustar 00root root 0000000 0000000 from rest_framework import serializers
from .models import Comment
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = (
'id',
'email',
)
class CommentDetailSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = (
'id',
'email',
'content',
) drf-extensions-0.7.1/tests_app/tests/functional/mixins/detail_serializer_mixin/tests.py 0000664 0000000 0000000 00000007446 14100726230 0031725 0 ustar 00root root 0000000 0000000 import datetime
from django.test import TestCase, override_settings
# todo: use from rest_framework when released
from rest_framework.test import APIRequestFactory
from .models import Comment
factory = APIRequestFactory()
@override_settings(ROOT_URLCONF='tests_app.tests.functional.mixins.detail_serializer_mixin.urls')
class DetailSerializerMixinTest_serializer_detail_class(TestCase):
def setUp(self):
self.comment = Comment.objects.create(
id=1,
email='example@ya.ru',
content='Hello world',
created=datetime.datetime.now()
)
def test_serializer_class_response(self):
resp = self.client.get('/comments/')
expected = [{
'id': 1,
'email': 'example@ya.ru'
}]
self.assertEqual(resp.data, expected)
def test_serializer_detail_class_response(self):
resp = self.client.get('/comments/1/')
expected = {
'id': 1,
'email': 'example@ya.ru',
'content': 'Hello world',
}
self.assertEqual(resp.data, expected, 'should use detail serializer for detail endpoint')
def test_view_with_mixin_and_without__serializer_detail_class__should_raise_exception(self):
msg = "'CommentWithoutDetailSerializerClassViewSet' should include a 'serializer_detail_class' attribute"
self.assertRaisesMessage(AssertionError, msg, self.client.get, '/comments-2/')
@override_settings(ROOT_URLCONF='tests_app.tests.functional.mixins.detail_serializer_mixin.urls')
class DetailSerializerMixin_queryset_detail(TestCase):
def setUp(self):
self.comments = [
Comment.objects.create(
id=1,
email='example@ya.ru',
content='Hello world',
created=datetime.datetime.now()
),
Comment.objects.create(
id=2,
email='example2@ya.ru',
content='Hello world 2',
created=datetime.datetime.now()
),
]
def test_list_should_use_default_queryset_method(self):
resp = self.client.get('/comments-3/')
expected = [{
'id': 2,
'email': 'example2@ya.ru'
}]
self.assertEqual(resp.data, expected)
def test_detail_view_should_use_default_queryset_if_queryset_detail_not_specified(self):
resp = self.client.get('/comments-3/1/')
self.assertEqual(resp.status_code, 404)
resp = self.client.get('/comments-3/2/')
expected = {
'id': 2,
'email': 'example2@ya.ru',
'content': 'Hello world 2',
}
self.assertEqual(resp.data, expected)
def test_list_should_use_default_queryset_method_if_queryset_detail_specified(self):
resp = self.client.get('/comments-4/')
expected = [{
'id': 2,
'email': 'example2@ya.ru'
}]
self.assertEqual(resp.data, expected)
def test_detail_view_should_use_custom_queryset_if_queryset_detail_specified(self):
resp = self.client.get('/comments-4/2/')
self.assertEqual(resp.status_code, 404)
resp = self.client.get('/comments-4/1/')
expected = {
'id': 1,
'email': 'example@ya.ru',
'content': 'Hello world',
}
self.assertEqual(resp.data, expected)
def test_nested_model_view_with_mixin_should_use_get_detail_queryset(self):
"""
Regression tests for https://github.com/chibisov/drf-extensions/pull/24
"""
resp = self.client.get('/comments-5/1/')
expected = {
'id': 1,
'email': 'example@ya.ru',
'content': 'Hello world',
}
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data, expected)
drf-extensions-0.7.1/tests_app/tests/functional/mixins/detail_serializer_mixin/urls.py 0000664 0000000 0000000 00000001313 14100726230 0031533 0 ustar 00root root 0000000 0000000 from rest_framework import routers
from .views import (
CommentViewSet,
CommentWithoutDetailSerializerClassViewSet,
CommentWithIdTwoViewSet,
CommentWithIdTwoAndIdOneForDetailViewSet,
CommentWithDetailSerializerAndNoArgsForGetQuerySetViewSet
)
viewset_router = routers.DefaultRouter()
viewset_router.register('comments', CommentViewSet)
viewset_router.register('comments-2', CommentWithoutDetailSerializerClassViewSet)
viewset_router.register('comments-3', CommentWithIdTwoViewSet)
viewset_router.register('comments-4', CommentWithIdTwoAndIdOneForDetailViewSet)
viewset_router.register('comments-5', CommentWithDetailSerializerAndNoArgsForGetQuerySetViewSet)
urlpatterns = viewset_router.urls
drf-extensions-0.7.1/tests_app/tests/functional/mixins/detail_serializer_mixin/views.py 0000664 0000000 0000000 00000003041 14100726230 0031703 0 ustar 00root root 0000000 0000000 from rest_framework import viewsets
from rest_framework_extensions.mixins import DetailSerializerMixin
from .models import Comment
from .serializers import CommentSerializer, CommentDetailSerializer
class CommentViewSet(DetailSerializerMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = CommentSerializer
serializer_detail_class = CommentDetailSerializer
queryset = Comment.objects.all()
class CommentWithoutDetailSerializerClassViewSet(DetailSerializerMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = CommentSerializer
queryset = Comment.objects.all()
class CommentWithIdTwoViewSet(DetailSerializerMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = CommentSerializer
serializer_detail_class = CommentDetailSerializer
queryset = Comment.objects.filter(id=2)
class CommentWithIdTwoAndIdOneForDetailViewSet(DetailSerializerMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = CommentSerializer
serializer_detail_class = CommentDetailSerializer
queryset = Comment.objects.filter(id=2)
queryset_detail = Comment.objects.filter(id=1)
class CommentWithDetailSerializerAndNoArgsForGetQuerySetViewSet(DetailSerializerMixin, viewsets.ModelViewSet):
"""
For regression tests https://github.com/chibisov/drf-extensions/pull/24
"""
serializer_class = CommentSerializer
serializer_detail_class = CommentDetailSerializer
queryset = Comment.objects.all()
queryset_detail = Comment.objects.filter(id=1)
def get_queryset(self):
return super().get_queryset()
drf-extensions-0.7.1/tests_app/tests/functional/mixins/list_destroy_model_mixin/ 0000775 0000000 0000000 00000000000 14100726230 0030407 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/mixins/list_destroy_model_mixin/__init__.py 0000664 0000000 0000000 00000000001 14100726230 0032507 0 ustar 00root root 0000000 0000000
drf-extensions-0.7.1/tests_app/tests/functional/mixins/list_destroy_model_mixin/models.py 0000664 0000000 0000000 00000000164 14100726230 0032245 0 ustar 00root root 0000000 0000000 from django.db import models
class CommentForListDestroyModelMixin(models.Model):
email = models.EmailField()
drf-extensions-0.7.1/tests_app/tests/functional/mixins/list_destroy_model_mixin/tests.py 0000664 0000000 0000000 00000006360 14100726230 0032130 0 ustar 00root root 0000000 0000000 from django.test import override_settings
from rest_framework.test import APITestCase
from rest_framework_extensions.settings import extensions_api_settings
from rest_framework_extensions import utils
from .models import CommentForListDestroyModelMixin as Comment
from tests_app.testutils import override_extensions_api_settings
@override_settings(ROOT_URLCONF='tests_app.tests.functional.mixins.list_destroy_model_mixin.urls')
class ListDestroyModelMixinTest(APITestCase):
def setUp(self):
self.comments = [
Comment.objects.create(
id=1,
email='example@ya.ru'
),
Comment.objects.create(
id=2,
email='example@gmail.com'
)
]
self.protection_headers = {
utils.prepare_header_name(extensions_api_settings.DEFAULT_BULK_OPERATION_HEADER_NAME): 'true'
}
def test_simple_response(self):
resp = self.client.get('/comments/')
expected = [
{
'id': 1,
'email': 'example@ya.ru'
},
{
'id': 2,
'email': 'example@gmail.com'
}
]
self.assertEqual(resp.data, expected)
def test_filter_works(self):
resp = self.client.get('/comments/?id=1')
expected = [
{
'id': 1,
'email': 'example@ya.ru'
}
]
self.assertEqual(resp.data, expected)
def test_destroy_instance(self):
resp = self.client.delete('/comments/1/')
self.assertEqual(resp.status_code, 204)
self.assertFalse(1 in Comment.objects.values_list('pk', flat=True))
def test_bulk_destroy__without_protection_header(self):
resp = self.client.delete('/comments/')
self.assertEqual(resp.status_code, 400)
expected_message = {
'detail': 'Header \'{0}\' should be provided for bulk operation.'.format(
extensions_api_settings.DEFAULT_BULK_OPERATION_HEADER_NAME
)
}
self.assertEqual(resp.data, expected_message)
def test_bulk_destroy__with_protection_header(self):
resp = self.client.delete('/comments/', **self.protection_headers)
self.assertEqual(resp.status_code, 204)
self.assertEqual(Comment.objects.count(), 0)
@override_extensions_api_settings(DEFAULT_BULK_OPERATION_HEADER_NAME=None)
def test_bulk_destroy__without_protection_header__and_with_turned_off_protection_header(self):
resp = self.client.delete('/comments/')
self.assertEqual(resp.status_code, 204)
self.assertEqual(Comment.objects.count(), 0)
def test_bulk_destroy__should_destroy_filtered_queryset(self):
resp = self.client.delete('/comments/?id=1', **self.protection_headers)
self.assertEqual(resp.status_code, 204)
self.assertEqual(Comment.objects.count(), 1)
self.assertEqual(Comment.objects.all()[0], self.comments[1])
def test_bulk_destroy__should_not_destroy_if_client_has_no_permissions(self):
resp = self.client.delete('/comments-with-permission/', **self.protection_headers)
self.assertEqual(resp.status_code, 404)
self.assertEqual(Comment.objects.count(), 2)
drf-extensions-0.7.1/tests_app/tests/functional/mixins/list_destroy_model_mixin/urls.py 0000664 0000000 0000000 00000000472 14100726230 0031751 0 ustar 00root root 0000000 0000000 from rest_framework import routers
from .views import CommentViewSet, CommentViewSetWithPermissions
viewset_router = routers.DefaultRouter()
viewset_router.register('comments', CommentViewSet)
viewset_router.register('comments-with-permissions', CommentViewSetWithPermissions)
urlpatterns = viewset_router.urls
drf-extensions-0.7.1/tests_app/tests/functional/mixins/list_destroy_model_mixin/views.py 0000664 0000000 0000000 00000001652 14100726230 0032122 0 ustar 00root root 0000000 0000000 import django_filters
from rest_framework import viewsets, serializers
from rest_framework import filters
from rest_framework.permissions import DjangoModelPermissions
from rest_framework_extensions.bulk_operations.mixins import ListDestroyModelMixin
from .models import CommentForListDestroyModelMixin as Comment
class CommentFilter(django_filters.FilterSet):
class Meta:
model = Comment
fields = [
'id'
]
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = '__all__'
class CommentViewSet(ListDestroyModelMixin, viewsets.ModelViewSet):
queryset = Comment.objects.all()
serializer_class = CommentSerializer
filter_backends = (django_filters.rest_framework.DjangoFilterBackend,)
filterset_class = CommentFilter
class CommentViewSetWithPermissions(CommentViewSet):
permission_classes = (DjangoModelPermissions,)
drf-extensions-0.7.1/tests_app/tests/functional/mixins/list_update_model_mixin/ 0000775 0000000 0000000 00000000000 14100726230 0030200 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/mixins/list_update_model_mixin/__init__.py 0000664 0000000 0000000 00000000001 14100726230 0032300 0 ustar 00root root 0000000 0000000
drf-extensions-0.7.1/tests_app/tests/functional/mixins/list_update_model_mixin/models.py 0000664 0000000 0000000 00000000561 14100726230 0032037 0 ustar 00root root 0000000 0000000 from django.db import models
class CommentForListUpdateModelMixin(models.Model):
email = models.EmailField()
class UserForListUpdateModelMixin(models.Model):
email = models.EmailField()
name = models.CharField(max_length=10)
age = models.IntegerField()
last_name = models.CharField(max_length=10)
password = models.CharField(max_length=100)
drf-extensions-0.7.1/tests_app/tests/functional/mixins/list_update_model_mixin/serializers.py 0000664 0000000 0000000 00000001210 14100726230 0033100 0 ustar 00root root 0000000 0000000 from rest_framework import serializers
from .models import (
UserForListUpdateModelMixin as User,
CommentForListUpdateModelMixin as Comment,
)
class UserSerializer(serializers.ModelSerializer):
surname = serializers.CharField(source='last_name')
class Meta:
model = User
extra_kwargs = {'password': {'write_only': True}}
read_only_fields = ('name',)
fields = [
'id',
'age',
'name',
'surname',
'password'
]
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = '__all__'
drf-extensions-0.7.1/tests_app/tests/functional/mixins/list_update_model_mixin/tests.py 0000664 0000000 0000000 00000017171 14100726230 0031723 0 ustar 00root root 0000000 0000000 import json
import unittest
import django
from django.test import override_settings
from rest_framework.test import APITestCase
from rest_framework_extensions.settings import extensions_api_settings
from rest_framework_extensions import utils
from .models import (
CommentForListUpdateModelMixin as Comment,
UserForListUpdateModelMixin as User
)
from tests_app.testutils import override_extensions_api_settings
@override_settings(ROOT_URLCONF='tests_app.tests.functional.mixins.list_update_model_mixin.urls')
class ListUpdateModelMixinTest(APITestCase):
def setUp(self):
self.comments = [
Comment.objects.create(
id=1,
email='example@ya.ru'
),
Comment.objects.create(
id=2,
email='example@gmail.com'
)
]
self.protection_headers = {
utils.prepare_header_name(extensions_api_settings.DEFAULT_BULK_OPERATION_HEADER_NAME): 'true'
}
self.patch_data = {
'email': 'example@yandex.ru'
}
def test_simple_response(self):
resp = self.client.get('/comments/')
expected = [
{
'id': 1,
'email': 'example@ya.ru'
},
{
'id': 2,
'email': 'example@gmail.com'
}
]
self.assertEqual(resp.data, expected)
def test_filter_works(self):
resp = self.client.get('/comments/?id=1')
expected = [
{
'id': 1,
'email': 'example@ya.ru'
}
]
self.assertEqual(resp.data, expected)
def test_update_instance(self):
data = {
'id': 1,
'email': 'example@yandex.ru'
}
resp = self.client.put('/comments/1/', data=json.dumps(data), content_type='application/json')
self.assertEqual(resp.status_code, 200)
self.assertEqual(Comment.objects.get(pk=1).email, 'example@yandex.ru')
def test_partial_update_instance(self):
data = {
'id': 1,
'email': 'example@yandex.ru'
}
resp = self.client.patch('/comments/1/', data=json.dumps(data), content_type='application/json')
self.assertEqual(resp.status_code, 200)
self.assertEqual(Comment.objects.get(pk=1).email, 'example@yandex.ru')
def test_bulk_partial_update__without_protection_header(self):
resp = self.client.patch('/comments/', data=json.dumps(self.patch_data), content_type='application/json')
self.assertEqual(resp.status_code, 400)
expected_message = {
'detail': 'Header \'{0}\' should be provided for bulk operation.'.format(
extensions_api_settings.DEFAULT_BULK_OPERATION_HEADER_NAME
)
}
self.assertEqual(resp.data, expected_message)
def test_bulk_partial_update__with_protection_header(self):
resp = self.client.patch('/comments/', data=json.dumps(self.patch_data), content_type='application/json', **self.protection_headers)
self.assertEqual(resp.status_code, 204)
for comment in Comment.objects.all():
self.assertEqual(comment.email, self.patch_data['email'])
@override_extensions_api_settings(DEFAULT_BULK_OPERATION_HEADER_NAME=None)
def test_bulk_partial_update__without_protection_header__and_with_turned_off_protection_header(self):
resp = self.client.patch('/comments/', data=json.dumps(self.patch_data), content_type='application/json', **self.protection_headers)
self.assertEqual(resp.status_code, 204)
for comment in Comment.objects.all():
self.assertEqual(comment.email, self.patch_data['email'])
def test_bulk_partial_update__should_update_filtered_queryset(self):
resp = self.client.patch('/comments/?id=1', data=json.dumps(self.patch_data), content_type='application/json', **self.protection_headers)
self.assertEqual(resp.status_code, 204)
self.assertEqual(Comment.objects.get(pk=1).email, self.patch_data['email'])
self.assertEqual(Comment.objects.get(pk=2).email, self.comments[1].email)
def test_bulk_partial_update__should_not_update_if_client_has_no_permissions(self):
resp = self.client.patch('/comments-with-permission/', data=json.dumps(self.patch_data), content_type='application/json', **self.protection_headers)
self.assertEqual(resp.status_code, 404)
for i, comment in enumerate(Comment.objects.all()):
self.assertEqual(comment.email, self.comments[i].email)
@override_settings(ROOT_URLCONF='tests_app.tests.functional.mixins.list_update_model_mixin.urls')
class ListUpdateModelMixinTestBehaviour__serializer_fields(APITestCase):
def setUp(self):
self.user = User.objects.create(
id=1,
name='Gennady',
age=24,
last_name='Chibisov',
email='example@ya.ru',
password='somepassword'
)
self.headers = {
utils.prepare_header_name(extensions_api_settings.DEFAULT_BULK_OPERATION_HEADER_NAME): 'true'
}
def get_fresh_user(self):
return User.objects.get(pk=self.user.pk)
def test_simple_response(self):
resp = self.client.get('/users/')
expected = [
{
'id': 1,
'age': 24,
'name': 'Gennady',
'surname': 'Chibisov'
}
]
self.assertEqual(resp.data, expected)
def test_invalid_for_db_data(self):
data = {
'age': 'Not integer value'
}
try:
resp = self.client.patch('/users/', data=json.dumps(data), content_type='application/json', **self.headers)
except ValueError:
self.fail('Errors with invalid for DB data should be caught')
else:
self.assertEqual(resp.status_code, 400)
if django.VERSION < (3, 0, 0):
expected_message = {
'detail': "invalid literal for int() with base 10: 'Not integer value'"
}
else:
expected_message = {
'detail': "Field 'age' expected a number but got 'Not integer value'."
}
self.assertEqual(resp.data, expected_message)
def test_should_use_source_if_it_set_in_serializer(self):
data = {
'surname': 'Ivanov'
}
resp = self.client.patch('/users/', data=json.dumps(data), content_type='application/json', **self.headers)
self.assertEqual(resp.status_code, 204)
self.assertEqual(self.get_fresh_user().last_name, data['surname'])
def test_should_update_write_only_fields(self):
data = {
'password': '123'
}
resp = self.client.patch('/users/', data=json.dumps(data), content_type='application/json', **self.headers)
self.assertEqual(resp.status_code, 204)
self.assertEqual(self.get_fresh_user().password, data['password'])
def test_should_not_update_read_only_fields(self):
data = {
'name': 'Ivan'
}
resp = self.client.patch('/users/', data=json.dumps(data), content_type='application/json', **self.headers)
self.assertEqual(resp.status_code, 204)
self.assertEqual(self.get_fresh_user().name, self.user.name)
def test_should_not_update_hidden_fields(self):
data = {
'email': 'example@gmail.com'
}
resp = self.client.patch('/users/', data=json.dumps(data), content_type='application/json', **self.headers)
self.assertEqual(resp.status_code, 204)
self.assertEqual(self.get_fresh_user().email, self.user.email)
drf-extensions-0.7.1/tests_app/tests/functional/mixins/list_update_model_mixin/urls.py 0000664 0000000 0000000 00000000565 14100726230 0031545 0 ustar 00root root 0000000 0000000 from rest_framework import routers
from .views import CommentViewSet, CommentViewSetWithPermissions, UserViewSet
viewset_router = routers.DefaultRouter()
viewset_router.register('comments', CommentViewSet)
viewset_router.register('comments-with-permissions', CommentViewSetWithPermissions)
viewset_router.register('users', UserViewSet)
urlpatterns = viewset_router.urls
drf-extensions-0.7.1/tests_app/tests/functional/mixins/list_update_model_mixin/views.py 0000664 0000000 0000000 00000002004 14100726230 0031703 0 ustar 00root root 0000000 0000000 import django_filters
from rest_framework import viewsets
from rest_framework import filters
from rest_framework.permissions import DjangoModelPermissions
from rest_framework_extensions.mixins import ListUpdateModelMixin
from .models import (
CommentForListUpdateModelMixin as Comment,
UserForListUpdateModelMixin as User
)
from .serializers import UserSerializer, CommentSerializer
class CommentFilter(django_filters.FilterSet):
class Meta:
model = Comment
fields = [
'id'
]
class CommentViewSet(ListUpdateModelMixin, viewsets.ModelViewSet):
queryset = Comment.objects.all()
serializer_class = CommentSerializer
filter_backends = (django_filters.rest_framework.DjangoFilterBackend,)
filterset_class = CommentFilter
class CommentViewSetWithPermissions(CommentViewSet):
permission_classes = (DjangoModelPermissions,)
class UserViewSet(ListUpdateModelMixin, viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer drf-extensions-0.7.1/tests_app/tests/functional/mixins/paginate_by_max_mixin/ 0000775 0000000 0000000 00000000000 14100726230 0027632 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/mixins/paginate_by_max_mixin/__init__.py 0000664 0000000 0000000 00000000001 14100726230 0031732 0 ustar 00root root 0000000 0000000
drf-extensions-0.7.1/tests_app/tests/functional/mixins/paginate_by_max_mixin/models.py 0000664 0000000 0000000 00000000401 14100726230 0031462 0 ustar 00root root 0000000 0000000 from django.db import models
class CommentForPaginateByMaxMixin(models.Model):
email = models.EmailField()
content = models.CharField(max_length=200)
created = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['id']
drf-extensions-0.7.1/tests_app/tests/functional/mixins/paginate_by_max_mixin/pagination.py 0000664 0000000 0000000 00000000675 14100726230 0032345 0 ustar 00root root 0000000 0000000 from rest_framework_extensions.mixins import PaginateByMaxMixin
from rest_framework.pagination import PageNumberPagination
class WithMaxPagination(PaginateByMaxMixin, PageNumberPagination):
page_size = 10
page_size_query_param = 'limit'
max_page_size = 20
class FlexiblePagination(PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size'
class FixedPagination(PageNumberPagination):
page_size = 10
drf-extensions-0.7.1/tests_app/tests/functional/mixins/paginate_by_max_mixin/serializers.py 0000664 0000000 0000000 00000000422 14100726230 0032536 0 ustar 00root root 0000000 0000000 from rest_framework import serializers
from .models import CommentForPaginateByMaxMixin
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = CommentForPaginateByMaxMixin
fields = (
'id',
'email',
)
drf-extensions-0.7.1/tests_app/tests/functional/mixins/paginate_by_max_mixin/tests.py 0000664 0000000 0000000 00000005315 14100726230 0031352 0 ustar 00root root 0000000 0000000 import datetime
from django.test import TestCase, override_settings
from .models import CommentForPaginateByMaxMixin
@override_settings(ROOT_URLCONF='tests_app.tests.functional.mixins.paginate_by_max_mixin.urls')
class PaginateByMaxMixinTest(TestCase):
def setUp(self):
for i in range(30):
CommentForPaginateByMaxMixin.objects.create(
email='example@ya.ru',
content='Hello world',
created=datetime.datetime.now()
)
def test_default_page_size(self):
resp = self.client.get('/comments/')
self.assertEqual(len(resp.data['results']), 10)
def test_custom_page_size__less_then_maximum(self):
resp = self.client.get('/comments/?limit=15')
self.assertEqual(len(resp.data['results']), 15)
def test_custom_page_size__more_then_maximum(self):
resp = self.client.get('/comments/?limit=25')
self.assertEqual(len(resp.data['results']), 20)
def test_custom_page_size_with_max_value(self):
resp = self.client.get('/comments/?limit=max')
self.assertEqual(len(resp.data['results']), 20)
def test_custom_page_size_with_max_value__for_view_without__paginate_by_param__attribute(self):
resp = self.client.get(
'/comments-without-paginate-by-param-attribute/?page_size=max')
self.assertEqual(len(resp.data['results']), 10)
def test_custom_page_size_with_max_value__for_view_without__max_paginate_by__attribute(self):
resp = self.client.get(
'/comments-without-max-paginate-by-attribute/?page_size=max')
self.assertEqual(len(resp.data['results']), 10)
@override_settings(ROOT_URLCONF='tests_app.tests.functional.mixins.paginate_by_max_mixin.urls')
class PaginateByMaxMixinTestBehavior__should_not_affect_view_if_DRF_does_not_supports__max_paginate_by(TestCase):
def setUp(self):
for i in range(30):
CommentForPaginateByMaxMixin.objects.create(
email='example@ya.ru',
content='Hello world',
created=datetime.datetime.now()
)
def test_default_page_size(self):
resp = self.client.get('/comments/')
self.assertEqual(len(resp.data['results']), 10)
def test_custom_page_size__less_then_maximum(self):
resp = self.client.get('/comments/?limit=15')
self.assertEqual(len(resp.data['results']), 15)
def test_custom_page_size__more_then_maximum(self):
resp = self.client.get('/comments/?limit=25')
self.assertEqual(len(resp.data['results']), 20)
def test_custom_page_size_with_max_value(self):
resp = self.client.get('/comments/?limit=max')
self.assertEqual(len(resp.data['results']), 20)
drf-extensions-0.7.1/tests_app/tests/functional/mixins/paginate_by_max_mixin/urls.py 0000664 0000000 0000000 00000001014 14100726230 0031165 0 ustar 00root root 0000000 0000000 from rest_framework import routers
from .views import (
CommentViewSet,
CommentWithoutPaginateByParamViewSet,
CommentWithoutMaxPaginateByAttributeViewSet,
)
viewset_router = routers.DefaultRouter()
viewset_router.register('comments', CommentViewSet)
viewset_router.register('comments-without-paginate-by-param-attribute', CommentWithoutPaginateByParamViewSet)
viewset_router.register('comments-without-max-paginate-by-attribute', CommentWithoutMaxPaginateByAttributeViewSet)
urlpatterns = viewset_router.urls
drf-extensions-0.7.1/tests_app/tests/functional/mixins/paginate_by_max_mixin/views.py 0000664 0000000 0000000 00000001532 14100726230 0031342 0 ustar 00root root 0000000 0000000 from rest_framework import viewsets
from .pagination import WithMaxPagination, FixedPagination, FlexiblePagination
from .models import CommentForPaginateByMaxMixin
from .serializers import CommentSerializer
class CommentViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = CommentSerializer
pagination_class = WithMaxPagination
queryset = CommentForPaginateByMaxMixin.objects.all().order_by('id')
class CommentWithoutPaginateByParamViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = CommentSerializer
pagination_class = FixedPagination
queryset = CommentForPaginateByMaxMixin.objects.all()
class CommentWithoutMaxPaginateByAttributeViewSet(viewsets.ReadOnlyModelViewSet):
pagination_class = FlexiblePagination
serializer_class = CommentSerializer
queryset = CommentForPaginateByMaxMixin.objects.all()
drf-extensions-0.7.1/tests_app/tests/functional/models.py 0000664 0000000 0000000 00000001001 14100726230 0023615 0 ustar 00root root 0000000 0000000 # from .concurrency.conditional_request.models import *
from .key_constructor.bits.models import *
from .mixins.detail_serializer_mixin.models import *
from .mixins.list_destroy_model_mixin.models import *
from .mixins.list_update_model_mixin.models import *
from .mixins.paginate_by_max_mixin.models import *
from .permissions.extended_django_object_permissions.models import *
from .routers.models import *
from .routers.extended_default_router.models import *
from .routers.nested_router_mixin.models import *
drf-extensions-0.7.1/tests_app/tests/functional/permissions/ 0000775 0000000 0000000 00000000000 14100726230 0024343 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/permissions/__init__.py 0000664 0000000 0000000 00000000001 14100726230 0026443 0 ustar 00root root 0000000 0000000
drf-extensions-0.7.1/tests_app/tests/functional/permissions/extended_django_object_permissions/ 0000775 0000000 0000000 00000000000 14100726230 0033446 5 ustar 00root root 0000000 0000000 __init__.py 0000664 0000000 0000000 00000000001 14100726230 0035467 0 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/permissions/extended_django_object_permissions
models.py 0000664 0000000 0000000 00000000532 14100726230 0035224 0 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/permissions/extended_django_object_permissions import django
from django.db import models
class PermissionsComment(models.Model):
text = models.CharField(max_length=100)
class Meta:
if django.VERSION < (2, 1):
permissions = (
('view_permissionscomment', 'Can view comment'),
# add, change, delete built in to django
)
tests.py 0000664 0000000 0000000 00000022375 14100726230 0035114 0 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/permissions/extended_django_object_permissions import json
from django.contrib.auth.models import User, Group, Permission
from django.contrib.contenttypes.models import ContentType
from django.test import override_settings
from rest_framework import status
from rest_framework.test import APITestCase
from tests_app.testutils import basic_auth_header
from .models import PermissionsComment
class ExtendedDjangoObjectPermissionTestMixin:
def setUp(self):
from guardian.shortcuts import assign_perm
# create users
create = User.objects.create_user
users = {
'fullaccess': create('fullaccess', 'fullaccess@example.com', 'password'),
'readonly': create('readonly', 'readonly@example.com', 'password'),
'writeonly': create('writeonly', 'writeonly@example.com', 'password'),
'deleteonly': create('deleteonly', 'deleteonly@example.com', 'password'),
}
# create custom permission
Permission.objects.get_or_create(
codename='view_permissionscomment',
content_type=ContentType.objects.get_for_model(PermissionsComment),
defaults={'name': 'Can view comment'},
)
# give everyone model level permissions, as we are not testing those
everyone = Group.objects.create(name='everyone')
model_name = PermissionsComment._meta.model_name
app_label = PermissionsComment._meta.app_label
f = '{0}_{1}'.format
perms = {
'view': f('view', model_name),
'change': f('change', model_name),
'delete': f('delete', model_name)
}
for perm in perms.values():
perm = '{0}.{1}'.format(app_label, perm)
assign_perm(perm, everyone)
everyone.user_set.add(*users.values())
# appropriate object level permissions
readers = Group.objects.create(name='readers')
writers = Group.objects.create(name='writers')
deleters = Group.objects.create(name='deleters')
model = PermissionsComment.objects.create(text='foo', id=1)
assign_perm(perms['view'], readers, model)
assign_perm(perms['change'], writers, model)
assign_perm(perms['delete'], deleters, model)
readers.user_set.add(users['fullaccess'], users['readonly'])
writers.user_set.add(users['fullaccess'], users['writeonly'])
deleters.user_set.add(users['fullaccess'], users['deleteonly'])
self.credentials = {}
for user in users.values():
self.credentials[user.username] = basic_auth_header(user.username, 'password')
@override_settings(ROOT_URLCONF='tests_app.tests.functional.permissions.extended_django_object_permissions.urls')
class ExtendedDjangoObjectPermissionsTest_should_inherit_standard(ExtendedDjangoObjectPermissionTestMixin,
APITestCase):
# Delete
def test_can_delete_permissions(self):
response = self.client.delete(
'/comments/1/',
**{'HTTP_AUTHORIZATION': self.credentials['deleteonly']})
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
def test_cannot_delete_permissions(self):
response = self.client.delete(
'/comments/1/',
**{'HTTP_AUTHORIZATION': self.credentials['readonly']})
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
# Update
def test_can_update_permissions(self):
response = self.client.patch(
'/comments/1/',
content_type='application/json',
data=json.dumps({'text': 'foobar'}),
**{
'HTTP_AUTHORIZATION': self.credentials['writeonly']
}
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data.get('text'), 'foobar')
def test_cannot_update_permissions(self):
response = self.client.patch(
'/comments/1/',
content_type='application/json',
data=json.dumps({'text': 'foobar'}),
**{
'HTTP_AUTHORIZATION': self.credentials['deleteonly']
}
)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_cannot_update_permissions_non_existing(self):
response = self.client.patch(
'/comments/999/',
content_type='application/json',
data=json.dumps({'text': 'foobar'}),
**{
'HTTP_AUTHORIZATION': self.credentials['deleteonly']
}
)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
# Read
def test_can_read_permissions(self):
response = self.client.get(
'/comments/1/',
**{'HTTP_AUTHORIZATION': self.credentials['readonly']})
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_cannot_read_permissions(self):
response = self.client.get(
'/comments/1/',
**{'HTTP_AUTHORIZATION': self.credentials['writeonly']})
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
# Read list
def test_can_read_list_permissions(self):
response = self.client.get(
'/comments-permission-filter-backend/',
**{'HTTP_AUTHORIZATION': self.credentials['readonly']}
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data[0].get('id'), 1)
def test_cannot_read_list_permissions(self):
response = self.client.get(
'/comments-permission-filter-backend/',
**{'HTTP_AUTHORIZATION': self.credentials['writeonly']}
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertListEqual(response.data, [])
@override_settings(ROOT_URLCONF='tests_app.tests.functional.permissions.extended_django_object_permissions.urls')
class ExtendedDjangoObjectPermissionsTest_without_hiding_forbidden_objects(ExtendedDjangoObjectPermissionTestMixin,
APITestCase):
# Delete
def test_can_delete_permissions(self):
response = self.client.delete(
'/comments-without-hiding-forbidden-objects/1/',
**{'HTTP_AUTHORIZATION': self.credentials['deleteonly']}
)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
def test_cannot_delete_permissions(self):
response = self.client.delete(
'/comments-without-hiding-forbidden-objects/1/',
**{'HTTP_AUTHORIZATION': self.credentials['readonly']}
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
# Update
def test_can_update_permissions(self):
response = self.client.patch(
'/comments-without-hiding-forbidden-objects/1/',
content_type='application/json',
data=json.dumps({'text': 'foobar'}),
**{
'HTTP_AUTHORIZATION': self.credentials['writeonly']
}
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data.get('text'), 'foobar')
def test_cannot_update_permissions(self):
response = self.client.patch(
'/comments-without-hiding-forbidden-objects/1/',
content_type='application/json',
data=json.dumps({'text': 'foobar'}),
**{
'HTTP_AUTHORIZATION': self.credentials['deleteonly']
}
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_cannot_update_permissions_non_existing(self):
response = self.client.patch(
'/comments-without-hiding-forbidden-objects/999/',
content_type='application/json',
data=json.dumps({'text': 'foobar'}),
**{
'HTTP_AUTHORIZATION': self.credentials['deleteonly']
}
)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
# Read
def test_can_read_permissions(self):
response = self.client.get(
'/comments-without-hiding-forbidden-objects/1/',
**{'HTTP_AUTHORIZATION': self.credentials['readonly']}
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_cannot_read_permissions(self):
response = self.client.get(
'/comments-without-hiding-forbidden-objects/1/',
**{'HTTP_AUTHORIZATION': self.credentials['writeonly']}
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
# Read list
def test_can_read_list_permissions(self):
response = self.client.get(
'/comments-without-hiding-forbidden-objects-permission-filter-backend/',
**{'HTTP_AUTHORIZATION': self.credentials['readonly']}
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data[0].get('id'), 1)
def test_cannot_read_list_permissions(self):
response = self.client.get(
'/comments-without-hiding-forbidden-objects-permission-filter-backend/',
**{'HTTP_AUTHORIZATION': self.credentials['writeonly']}
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertListEqual(response.data, [])
urls.py 0000664 0000000 0000000 00000001367 14100726230 0034735 0 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/permissions/extended_django_object_permissions from rest_framework import routers
from .views import (
CommentViewSet,
CommentViewSetPermissionFilterBackend,
CommentViewSetWithoutHidingForbiddenObjects,
CommentViewSetWithoutHidingForbiddenObjectsPermissionFilterBackend
)
viewset_router = routers.DefaultRouter()
viewset_router.register('comments', CommentViewSet)
viewset_router.register('comments-permission-filter-backend', CommentViewSetPermissionFilterBackend)
viewset_router.register('comments-without-hiding-forbidden-objects', CommentViewSetWithoutHidingForbiddenObjects)
viewset_router.register(
'comments-without-hiding-forbidden-objects-permission-filter-backend',
CommentViewSetWithoutHidingForbiddenObjectsPermissionFilterBackend
)
urlpatterns = viewset_router.urls
views.py 0000664 0000000 0000000 00000003672 14100726230 0035106 0 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/permissions/extended_django_object_permissions from rest_framework import viewsets, serializers
from rest_framework import authentication
try:
# djangorestframework >= 3.9
from rest_framework_guardian.filters import DjangoObjectPermissionsFilter
except ImportError:
from rest_framework.filters import DjangoObjectPermissionsFilter
try:
from rest_framework_extensions.permissions import ExtendedDjangoObjectPermissions
except ImportError:
class ExtendedDjangoObjectPermissions:
pass
from .models import PermissionsComment
class CommentObjectPermissions(ExtendedDjangoObjectPermissions):
perms_map = {
'GET': ['%(app_label)s.view_%(model_name)s'],
'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
'HEAD': ['%(app_label)s.view_%(model_name)s'],
'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
}
class PermissionsCommentSerializer(serializers.ModelSerializer):
class Meta:
model = PermissionsComment
fields = '__all__'
class CommentObjectPermissionsWithoutHidingForbiddenObjects(CommentObjectPermissions):
hide_forbidden_for_read_objects = False
class CommentViewSet(viewsets.ModelViewSet):
queryset = PermissionsComment.objects.all()
serializer_class = PermissionsCommentSerializer
authentication_classes = [authentication.BasicAuthentication]
permission_classes = (CommentObjectPermissions,)
class CommentViewSetPermissionFilterBackend(CommentViewSet):
filter_backends = (DjangoObjectPermissionsFilter,)
class CommentViewSetWithoutHidingForbiddenObjects(CommentViewSet):
permission_classes = (CommentObjectPermissionsWithoutHidingForbiddenObjects,)
class CommentViewSetWithoutHidingForbiddenObjectsPermissionFilterBackend(CommentViewSetWithoutHidingForbiddenObjects):
filter_backends = (DjangoObjectPermissionsFilter,) drf-extensions-0.7.1/tests_app/tests/functional/routers/ 0000775 0000000 0000000 00000000000 14100726230 0023473 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/routers/__init__.py 0000664 0000000 0000000 00000000001 14100726230 0025573 0 ustar 00root root 0000000 0000000
drf-extensions-0.7.1/tests_app/tests/functional/routers/extended_default_router/ 0000775 0000000 0000000 00000000000 14100726230 0030377 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/routers/extended_default_router/__init__.py 0000664 0000000 0000000 00000000001 14100726230 0032477 0 ustar 00root root 0000000 0000000
drf-extensions-0.7.1/tests_app/tests/functional/routers/extended_default_router/models.py 0000664 0000000 0000000 00000000723 14100726230 0032236 0 ustar 00root root 0000000 0000000 from django.db import models
class DefaultRouterUserModel(models.Model):
name = models.CharField(max_length=10)
groups = models.ManyToManyField('DefaultRouterGroupModel', related_name='user_groups')
class DefaultRouterGroupModel(models.Model):
name = models.CharField(max_length=10)
permissions = models.ManyToManyField('DefaultRouterPermissionModel')
class DefaultRouterPermissionModel(models.Model):
name = models.CharField(max_length=10)
drf-extensions-0.7.1/tests_app/tests/functional/routers/extended_default_router/tests.py 0000664 0000000 0000000 00000001736 14100726230 0032122 0 ustar 00root root 0000000 0000000 from django.test import override_settings
from django.urls import NoReverseMatch
from rest_framework.test import APITestCase
@override_settings(ROOT_URLCONF='tests_app.tests.functional.routers.extended_default_router.urls')
class ExtendedDefaultRouterTestBehaviour(APITestCase):
def test_index_page(self):
try:
response = self.client.get('/')
except NoReverseMatch:
issue = 'https://github.com/chibisov/drf-extensions/issues/14'
self.fail('DefaultRouter tries to reverse nested routes and breaks with error. NoReverseMatch should be '
'handled for nested routes. They must be excluded from index page. ' + issue)
self.assertEqual(response.status_code, 200)
expected = {
'users': 'http://testserver/users/',
'groups': 'http://testserver/groups/',
'permissions': 'http://testserver/permissions/',
}
self.assertEqual(response.data, expected)
drf-extensions-0.7.1/tests_app/tests/functional/routers/extended_default_router/urls.py 0000664 0000000 0000000 00000001173 14100726230 0031740 0 ustar 00root root 0000000 0000000 from rest_framework_extensions.routers import ExtendedDefaultRouter
from .views import (
UserViewSet,
GroupViewSet,
PermissionViewSet,
)
router = ExtendedDefaultRouter()
# nested routes
(
router.register(r'users', UserViewSet)
.register(r'groups', GroupViewSet, 'users-group', parents_query_lookups=['user_groups'])
.register(r'permissions', PermissionViewSet, 'users-groups-permission', parents_query_lookups=['group__user', 'group'])
)
# simple routes
router.register(r'groups', GroupViewSet, 'group')
router.register(r'permissions', PermissionViewSet, 'permission')
urlpatterns = router.urls
drf-extensions-0.7.1/tests_app/tests/functional/routers/extended_default_router/views.py 0000664 0000000 0000000 00000001065 14100726230 0032110 0 ustar 00root root 0000000 0000000 from rest_framework.viewsets import ModelViewSet
from rest_framework_extensions.mixins import NestedViewSetMixin
from .models import (
DefaultRouterUserModel,
DefaultRouterGroupModel,
DefaultRouterPermissionModel,
)
class UserViewSet(NestedViewSetMixin, ModelViewSet):
queryset = DefaultRouterUserModel.objects.all()
class GroupViewSet(NestedViewSetMixin, ModelViewSet):
queryset = DefaultRouterGroupModel.objects.all()
class PermissionViewSet(NestedViewSetMixin, ModelViewSet):
queryset = DefaultRouterPermissionModel.objects.all()
drf-extensions-0.7.1/tests_app/tests/functional/routers/models.py 0000664 0000000 0000000 00000000232 14100726230 0025325 0 ustar 00root root 0000000 0000000 from django.db import models
class RouterTestModel(models.Model):
uuid = models.CharField(max_length=20)
text = models.CharField(max_length=200) drf-extensions-0.7.1/tests_app/tests/functional/routers/nested_router_mixin/ 0000775 0000000 0000000 00000000000 14100726230 0027561 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/routers/nested_router_mixin/__init__.py 0000664 0000000 0000000 00000000001 14100726230 0031661 0 ustar 00root root 0000000 0000000
drf-extensions-0.7.1/tests_app/tests/functional/routers/nested_router_mixin/models.py 0000664 0000000 0000000 00000002207 14100726230 0031417 0 ustar 00root root 0000000 0000000 from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
class NestedRouterMixinUserModel(models.Model):
email = models.EmailField(blank=True, null=True)
name = models.CharField(max_length=10)
groups = models.ManyToManyField(
'NestedRouterMixinGroupModel', related_name='user_groups')
class NestedRouterMixinGroupModel(models.Model):
name = models.CharField(max_length=10)
permissions = models.ManyToManyField('NestedRouterMixinPermissionModel')
class NestedRouterMixinPermissionModel(models.Model):
name = models.CharField(max_length=10)
class NestedRouterMixinTaskModel(models.Model):
title = models.CharField(max_length=30)
class NestedRouterMixinBookModel(models.Model):
title = models.CharField(max_length=30)
class NestedRouterMixinCommentModel(models.Model):
content_type = models.ForeignKey(
"contenttypes.ContentType",
blank=True,
null=True,
on_delete=models.CASCADE,
)
object_id = models.PositiveIntegerField(blank=True, null=True)
content_object = GenericForeignKey()
text = models.CharField(max_length=30)
drf-extensions-0.7.1/tests_app/tests/functional/routers/nested_router_mixin/serializers.py 0000664 0000000 0000000 00000002562 14100726230 0032474 0 ustar 00root root 0000000 0000000 from rest_framework import serializers
from .models import (
NestedRouterMixinUserModel as UserModel,
NestedRouterMixinGroupModel as GroupModel,
NestedRouterMixinPermissionModel as PermissionModel,
NestedRouterMixinTaskModel as TaskModel,
NestedRouterMixinBookModel as BookModel,
NestedRouterMixinCommentModel as CommentModel,
)
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = UserModel
fields = (
'id',
'name'
)
class GroupSerializer(serializers.ModelSerializer):
class Meta:
model = GroupModel
fields = (
'id',
'name'
)
class PermissionSerializer(serializers.ModelSerializer):
class Meta:
model = PermissionModel
fields = (
'id',
'name'
)
class TaskSerializer(serializers.ModelSerializer):
class Meta:
model = TaskModel
fields = (
'id',
'title'
)
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = BookModel
fields = (
'id',
'title'
)
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = CommentModel
fields = (
'id',
'content_type',
'object_id',
'text'
) drf-extensions-0.7.1/tests_app/tests/functional/routers/nested_router_mixin/tests.py 0000664 0000000 0000000 00000044576 14100726230 0031315 0 ustar 00root root 0000000 0000000 from django.test import override_settings
from rest_framework.test import APITestCase
from .models import (
NestedRouterMixinUserModel as UserModel,
NestedRouterMixinGroupModel as GroupModel,
NestedRouterMixinPermissionModel as PermissionModel,
NestedRouterMixinTaskModel as TaskModel,
NestedRouterMixinBookModel as BookModel,
NestedRouterMixinCommentModel as CommentModel
)
@override_settings(ROOT_URLCONF='tests_app.tests.functional.routers.nested_router_mixin.urls')
class NestedRouterMixinTestBehaviourBase(APITestCase):
def setUp(self):
self.users = {
'vova': UserModel.objects.create(id=1, name='vova'),
'gena': UserModel.objects.create(id=2, name='gena'),
}
self.groups = {
'users': GroupModel.objects.create(id=3, name='users'),
'admins': GroupModel.objects.create(id=4, name='admins'),
'super_admins': GroupModel.objects.create(id=5, name='super_admins'),
}
self.permissions = {
'read': PermissionModel.objects.create(id=6, name='read'),
'update': PermissionModel.objects.create(id=7, name='update'),
'delete': PermissionModel.objects.create(id=8, name='delete'),
}
# add permissions to groups
self.groups['users'].permissions.set([
self.permissions['read']
])
self.groups['admins'].permissions.set([
self.permissions['read'],
self.permissions['update'],
])
self.groups['super_admins'].permissions.set([
self.permissions['read'],
self.permissions['update'],
self.permissions['delete'],
])
# add groups to users
self.users['vova'].groups.set([
self.groups['users']
])
self.users['gena'].groups.set([
self.groups['admins'],
self.groups['super_admins'],
])
class NestedRouterMixinTestBehaviour__main_routes(NestedRouterMixinTestBehaviourBase):
def test_users(self):
response = self.client.get('/users/')
self.assertEqual(response.status_code, 200)
expected = [
{
'id': self.users['vova'].id,
'name': self.users['vova'].name
},
{
'id': self.users['gena'].id,
'name': self.users['gena'].name
},
]
self.assertEqual(response.data, expected)
def test_users_detail(self):
url = '/users/{0}/'.format(self.users['gena'].id)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
expected = {
'id': self.users['gena'].id,
'name': self.users['gena'].name
}
self.assertEqual(response.data, expected)
def test_users_groups(self):
url = '/users/{0}/groups/'.format(self.users['gena'].id)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
expected = [
{
'id': self.groups['admins'].id,
'name': self.groups['admins'].name
},
{
'id': self.groups['super_admins'].id,
'name': self.groups['super_admins'].name
}
]
msg = 'Groups should be filtered by user'
self.assertEqual(response.data, expected, msg=msg)
def test_users_groups_detail(self):
url = '/users/{user_pk}/groups/{group_pk}/'.format(
user_pk=self.users['gena'].id,
group_pk=self.groups['admins'].id
)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
expected = {
'id': self.groups['admins'].id,
'name': self.groups['admins'].name
}
self.assertEqual(response.data, expected)
def test_users_groups_detail__if_user_has_no_such_group(self):
url = '/users/{user_pk}/groups/{group_pk}/'.format(
user_pk=self.users['gena'].id,
group_pk=self.groups['users'].id
)
response = self.client.get(url)
msg = 'If user has no requested group it should return 404'
self.assertEqual(response.status_code, 404, msg=msg)
def test_simple_groups(self):
response = self.client.get('/groups/')
self.assertEqual(response.status_code, 200)
expected = [
{
'id': self.groups['users'].id,
'name': self.groups['users'].name
},
{
'id': self.groups['admins'].id,
'name': self.groups['admins'].name
},
{
'id': self.groups['super_admins'].id,
'name': self.groups['super_admins'].name
},
]
self.assertEqual(response.data, expected)
def test_simple_permissions(self):
response = self.client.get('/permissions/')
self.assertEqual(response.status_code, 200)
expected = [
{
'id': self.permissions['read'].id,
'name': self.permissions['read'].name
},
{
'id': self.permissions['update'].id,
'name': self.permissions['update'].name
},
{
'id': self.permissions['delete'].id,
'name': self.permissions['delete'].name
},
]
self.assertEqual(response.data, expected)
class NestedRouterMixinTestBehaviour__register_on_one_depth(NestedRouterMixinTestBehaviourBase):
def test_permissions(self):
response = self.client.get('/permissions/')
self.assertEqual(response.status_code, 200)
expected = [
{
'id': self.permissions['read'].id,
'name': self.permissions['read'].name
},
{
'id': self.permissions['update'].id,
'name': self.permissions['update'].name
},
{
'id': self.permissions['delete'].id,
'name': self.permissions['delete'].name
},
]
self.assertEqual(response.data, expected)
def test_permissions_detail(self):
url = '/permissions/{0}/'.format(self.permissions['read'].id)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
expected = {
'id': self.permissions['read'].id,
'name': self.permissions['read'].name
}
self.assertEqual(response.data, expected)
def test_permissions_groups(self):
url = '/permissions/{0}/groups/'.format(self.permissions['update'].id)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
expected = [
{
'id': self.groups['admins'].id,
'name': self.groups['admins'].name
},
{
'id': self.groups['super_admins'].id,
'name': self.groups['super_admins'].name
},
]
msg = 'Groups should be filtered by permission'
self.assertEqual(response.data, expected, msg=msg)
def test_permissions_users(self):
url = '/permissions/{0}/users/'.format(self.permissions['delete'].id)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
expected = [
{
'id': self.users['gena'].id,
'name': self.users['gena'].name
},
]
msg = 'Users should be filtered by group permissions'
self.assertEqual(response.data, expected, msg=msg)
class NestedRouterMixinTestBehaviour__actions_and_links(NestedRouterMixinTestBehaviourBase):
def test_users_list_action(self):
response = self.client.post('/users/users-list-action/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, 'users list action')
def test_users_action(self):
url = '/users/{0}/users-action/'.format(self.users['gena'].id)
response = self.client.post(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, 'users action')
def test_groups_list_link(self):
url = '/groups/groups-list-link/'
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, 'groups list link')
def test_groups_link(self):
url = '/groups/{0}/groups-link/'.format(
self.groups['admins'].id,
)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, 'groups link')
def test_users_groups_list_link(self):
url = '/users/{0}/groups/groups-list-link/'.format(self.users['gena'].id)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, 'groups list link')
def test_permissions_list_action(self):
url = '/permissions/permissions-list-action/'
response = self.client.post(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, 'permissions list action')
def test_permissions_action(self):
url = '/permissions/{0}/permissions-action/'.format(
self.permissions['delete'].id,
)
response = self.client.post(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, 'permissions action')
def test_users_groups_link(self):
url = '/users/{0}/groups/{1}/groups-link/'.format(
self.users['gena'].id,
self.groups['admins'].id,
)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, 'groups link')
def test_users_groups_permissions_list_action(self):
url = '/users/{0}/groups/{1}/permissions/permissions-list-action/'.format(
self.users['gena'].id,
self.groups['admins'].id,
)
response = self.client.post(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, 'permissions list action')
def test_users_groups_permissions_action(self):
url = '/users/{0}/groups/{1}/permissions/{2}/permissions-action/'.format(
self.users['gena'].id,
self.groups['admins'].id,
self.permissions['delete'].id,
)
response = self.client.post(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, 'permissions action')
@override_settings(ROOT_URLCONF='tests_app.tests.functional.routers.nested_router_mixin.urls_generic_relations')
class NestedRouterMixinTestBehaviour__generic_relations(APITestCase):
def setUp(self):
self.tasks = {
'one': TaskModel.objects.create(id=1, title='Task one'),
'two': TaskModel.objects.create(id=2, title='Task two'),
}
self.books = {
'one': BookModel.objects.create(id=1, title='Book one'),
'two': BookModel.objects.create(id=2, title='Book two'),
}
self.comments = {
'for_task_one': CommentModel.objects.create(
id=1,
content_object=self.tasks['one'],
text=u'Comment for task one'
),
'for_task_two': CommentModel.objects.create(
id=2,
content_object=self.tasks['two'],
text=u'Comment for task two'
),
'for_book_one': CommentModel.objects.create(
id=3,
content_object=self.books['one'],
text=u'Comment for book one'
),
'for_book_two': CommentModel.objects.create(
id=4,
content_object=self.books['two'],
text=u'Comment for book two'
),
}
def assertResult(self, response, result):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, result)
def test_comments_for_tasks(self):
url = '/tasks/{0}/comments/'.format(
self.tasks['one'].id,
)
response = self.client.get(url)
self.assertResult(response, [
{
'id': self.comments['for_task_one'].id,
'content_type': self.comments['for_task_one'].content_type.id,
'object_id': self.comments['for_task_one'].object_id,
'text': self.comments['for_task_one'].text,
}
])
url = '/tasks/{0}/comments/'.format(
self.tasks['two'].id,
)
response = self.client.get(url)
self.assertResult(response, [
{
'id': self.comments['for_task_two'].id,
'content_type': self.comments['for_task_two'].content_type.id,
'object_id': self.comments['for_task_two'].object_id,
'text': self.comments['for_task_two'].text,
}
])
def test_comments_for_books(self):
url = '/books/{0}/comments/'.format(
self.books['one'].id,
)
response = self.client.get(url)
self.assertResult(response, [
{
'id': self.comments['for_book_one'].id,
'content_type': self.comments['for_book_one'].content_type.id,
'object_id': self.comments['for_book_one'].object_id,
'text': self.comments['for_book_one'].text,
}
])
url = '/books/{0}/comments/'.format(
self.books['two'].id,
)
response = self.client.get(url)
self.assertResult(response, [
{
'id': self.comments['for_book_two'].id,
'content_type': self.comments['for_book_two'].content_type.id,
'object_id': self.comments['for_book_two'].object_id,
'text': self.comments['for_book_two'].text,
}
])
@override_settings(ROOT_URLCONF='tests_app.tests.functional.routers.nested_router_mixin.urls_parent_viewset_lookup')
class NestedRouterMixinTestBehaviour__parent_viewset_lookup(APITestCase):
def setUp(self):
self.users = {
'vova': UserModel.objects.create(id=1, name='vova', email='vova@example.com'),
'gena': UserModel.objects.create(id=2, name='gena', email='gena@example.com'),
}
self.groups = {
'users': GroupModel.objects.create(id=3, name='users'),
'admins': GroupModel.objects.create(id=4, name='admins'),
'super_admins': GroupModel.objects.create(id=5, name='super_admins'),
}
# add groups to users
self.users['vova'].groups.set([
self.groups['users']
])
self.users['gena'].groups.set([
self.groups['admins'],
self.groups['super_admins'],
])
def test_users_detail(self):
url = '/users/{0}/'.format(self.users['gena'].email)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
expected = {
'id': self.users['gena'].id,
'name': self.users['gena'].name
}
self.assertEqual(response.data, expected)
def test_users_groups(self):
url = '/users/{0}/groups/'.format(self.users['gena'].email)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
expected = [
{
'id': self.groups['admins'].id,
'name': self.groups['admins'].name
},
{
'id': self.groups['super_admins'].id,
'name': self.groups['super_admins'].name
}
]
msg = 'Groups should be filtered by user'
self.assertEqual(response.data, expected, msg=msg)
def test_users_groups_detail(self):
url = '/users/{user_email}/groups/{group_pk}/'.format(
user_email=self.users['gena'].email,
group_pk=self.groups['admins'].id
)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
expected = {
'id': self.groups['admins'].id,
'name': self.groups['admins'].name
}
self.assertEqual(response.data, expected)
def test_users_groups_detail__if_user_has_no_such_group(self):
url = '/users/{user_email}/groups/{group_pk}/'.format(
user_email=self.users['gena'].email,
group_pk=self.groups['users'].id
)
response = self.client.get(url)
msg = 'If user has no requested group it should return 404'
self.assertEqual(response.status_code, 404, msg=msg)
# class NestedRouterMixinTestBehaviour__generic_relations1(APITestCase):
# router = ExtendedSimpleRouter()
# # tasks route
# (
# router.register(r'tasks', TaskViewSet)
# .register(r'', TaskCommentViewSet, 'tasks-comment', parents_query_lookups=['object_id'])
# )
# # books route
# (
# router.register(r'books', BookViewSet)
# .register(r'', BookCommentViewSet, 'books-comment', parents_query_lookups=['object_id'])
# )
#
# urls = router.urls
#
# def setUp(self):
# self.tasks = {
# 'one': TaskModel.objects.create(id=1, title='Task one'),
# 'two': TaskModel.objects.create(id=2, title='Task two'),
# }
# self.books = {
# 'one': BookModel.objects.create(id=1, title='Book one'),
# 'two': BookModel.objects.create(id=2, title='Book two'),
# }
# self.comments = {
# 'for_task_one': CommentModel.objects.create(
# id=1,
# content_object=self.tasks['one'],
# text=u'Comment for task one'
# ),
# 'for_task_two': CommentModel.objects.create(
# id=2,
# content_object=self.tasks['two'],
# text=u'Comment for task two'
# ),
# 'for_book_one': CommentModel.objects.create(
# id=3,
# content_object=self.books['one'],
# text=u'Comment for book one'
# ),
# 'for_book_two': CommentModel.objects.create(
# id=4,
# content_object=self.books['two'],
# text=u'Comment for book two'
# ),
# }
#
# def test_me(self):
# print 'hell'
drf-extensions-0.7.1/tests_app/tests/functional/routers/nested_router_mixin/urls.py 0000664 0000000 0000000 00000001700 14100726230 0031116 0 ustar 00root root 0000000 0000000 from rest_framework_extensions.routers import ExtendedSimpleRouter
from .views import (
UserViewSet,
GroupViewSet,
PermissionViewSet,
)
router = ExtendedSimpleRouter()
# main routes
(
router.register(r'users', UserViewSet)
.register(r'groups', GroupViewSet, 'users-group', parents_query_lookups=['user_groups'])
.register(r'permissions', PermissionViewSet, 'users-groups-permission', parents_query_lookups=['group__user', 'group'])
)
# register on one depth
permissions_routes = router.register(r'permissions', PermissionViewSet)
permissions_routes.register(r'groups', GroupViewSet, 'permissions-group', parents_query_lookups=['permissions'])
permissions_routes.register(r'users', UserViewSet, 'permissions-user', parents_query_lookups=['groups__permissions'])
# simple routes
router.register(r'groups', GroupViewSet, 'group')
router.register(r'permissions', PermissionViewSet, 'permission')
urlpatterns = router.urls
urls_generic_relations.py 0000664 0000000 0000000 00000001071 14100726230 0034614 0 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/routers/nested_router_mixin from rest_framework_extensions.routers import ExtendedSimpleRouter
from .views import (
TaskViewSet,
TaskCommentViewSet,
BookViewSet,
BookCommentViewSet
)
router = ExtendedSimpleRouter()
# tasks route
(
router.register(r'tasks', TaskViewSet)
.register(r'comments', TaskCommentViewSet, 'tasks-comment', parents_query_lookups=['object_id'])
)
# books route
(
router.register(r'books', BookViewSet)
.register(r'comments', BookCommentViewSet, 'books-comment', parents_query_lookups=['object_id'])
)
urlpatterns = router.urls
urls_parent_viewset_lookup.py 0000664 0000000 0000000 00000000600 14100726230 0035545 0 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/functional/routers/nested_router_mixin from rest_framework_extensions.routers import ExtendedSimpleRouter
from .views import (
UserViewSetWithEmailLookup,
GroupViewSet,
)
router = ExtendedSimpleRouter()
# main routes
(
router.register(r'users', UserViewSetWithEmailLookup)
.register(r'groups', GroupViewSet, 'users-group', parents_query_lookups=['user_groups__email'])
)
urlpatterns = router.urls
drf-extensions-0.7.1/tests_app/tests/functional/routers/nested_router_mixin/views.py 0000664 0000000 0000000 00000006227 14100726230 0031277 0 ustar 00root root 0000000 0000000 from django.contrib.contenttypes.models import ContentType
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from rest_framework_extensions.mixins import NestedViewSetMixin
from .models import (
NestedRouterMixinUserModel as UserModel,
NestedRouterMixinGroupModel as GroupModel,
NestedRouterMixinPermissionModel as PermissionModel,
NestedRouterMixinTaskModel as TaskModel,
NestedRouterMixinBookModel as BookModel,
NestedRouterMixinCommentModel as CommentModel
)
from .serializers import (
UserSerializer,
GroupSerializer,
PermissionSerializer,
TaskSerializer,
BookSerializer,
CommentSerializer
)
class UserViewSet(NestedViewSetMixin, ModelViewSet):
queryset = UserModel.objects.all()
serializer_class = UserSerializer
@action(detail=False, methods=['post'], url_path='users-list-action')
def users_list_action(self, request, *args, **kwargs):
return Response('users list action')
@action(detail=True, methods=['post'], url_path='users-action')
def users_action(self, request, *args, **kwargs):
return Response('users action')
class GroupViewSet(NestedViewSetMixin, ModelViewSet):
queryset = GroupModel.objects.all()
serializer_class = GroupSerializer
@action(detail=False, url_path='groups-list-link')
def groups_list_link(self, request, *args, **kwargs):
return Response('groups list link')
@action(detail=True, url_path='groups-link')
def groups_link(self, request, *args, **kwargs):
return Response('groups link')
class PermissionViewSet(NestedViewSetMixin, ModelViewSet):
queryset = PermissionModel.objects.all()
serializer_class = PermissionSerializer
@action(detail=False, methods=['post'], url_path='permissions-list-action')
def permissions_list_action(self, request, *args, **kwargs):
return Response('permissions list action')
@action(detail=True, methods=['post'], url_path='permissions-action')
def permissions_action(self, request, *args, **kwargs):
return Response('permissions action')
class TaskViewSet(NestedViewSetMixin, ModelViewSet):
queryset = TaskModel.objects.all()
serializer_class = TaskSerializer
class BookViewSet(NestedViewSetMixin, ModelViewSet):
queryset = BookModel.objects.all()
serializer_class = BookSerializer
class CommentViewSet(NestedViewSetMixin, ModelViewSet):
queryset = CommentModel.objects.all()
serializer_class = CommentSerializer
class TaskCommentViewSet(CommentViewSet):
def get_queryset(self):
return super().get_queryset().filter(
content_type=ContentType.objects.get_for_model(TaskModel)
)
class BookCommentViewSet(CommentViewSet):
def get_queryset(self):
return super().get_queryset().filter(
content_type=ContentType.objects.get_for_model(BookModel)
)
class UserViewSetWithEmailLookup(NestedViewSetMixin, ModelViewSet):
queryset = UserModel.objects.all()
serializer_class = UserSerializer
lookup_field = 'email'
lookup_value_regex = r'[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+'
drf-extensions-0.7.1/tests_app/tests/functional/routers/tests.py 0000664 0000000 0000000 00000002630 14100726230 0025210 0 ustar 00root root 0000000 0000000 from django.test import TestCase
from rest_framework_extensions.routers import ExtendedSimpleRouter
from tests_app.testutils import get_url_pattern_by_regex_pattern
from .views import RouterViewSet
class TestTrailingSlashIncluded(TestCase):
def test_urls_have_trailing_slash_by_default(self):
router = ExtendedSimpleRouter()
router.register(r'router-viewset', RouterViewSet)
urls = router.urls
for exp in ['^router-viewset/$',
'^router-viewset/(?P[^/.]+)/$',
'^router-viewset/list_controller/$',
'^router-viewset/(?P[^/.]+)/detail_controller/$']:
msg = 'Should find url pattern with regexp %s' % exp
self.assertIsNotNone(get_url_pattern_by_regex_pattern(urls, exp), msg=msg)
class TestTrailingSlashRemoved(TestCase):
def test_urls_can_have_trailing_slash_removed(self):
router = ExtendedSimpleRouter(trailing_slash=False)
router.register(r'router-viewset', RouterViewSet)
urls = router.urls
for exp in ['^router-viewset$',
'^router-viewset/(?P[^/.]+)$',
'^router-viewset/list_controller$',
'^router-viewset/(?P[^/.]+)/detail_controller$']:
msg = 'Should find url pattern with regexp %s' % exp
self.assertIsNotNone(get_url_pattern_by_regex_pattern(urls, exp), msg=msg)
drf-extensions-0.7.1/tests_app/tests/functional/routers/views.py 0000664 0000000 0000000 00000000540 14100726230 0025201 0 ustar 00root root 0000000 0000000 from rest_framework import viewsets
from rest_framework.decorators import action
from .models import RouterTestModel
class RouterViewSet(viewsets.ModelViewSet):
queryset = RouterTestModel.objects.all()
@action(detail=True)
def detail_controller(self):
pass
@action(detail=False)
def list_controller(self):
pass
drf-extensions-0.7.1/tests_app/tests/unit/ 0000775 0000000 0000000 00000000000 14100726230 0020605 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/unit/__init__.py 0000664 0000000 0000000 00000000000 14100726230 0022704 0 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/unit/_etag/ 0000775 0000000 0000000 00000000000 14100726230 0021664 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/unit/_etag/__init__.py 0000664 0000000 0000000 00000000000 14100726230 0023763 0 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/unit/_etag/decorators/ 0000775 0000000 0000000 00000000000 14100726230 0024031 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/unit/_etag/decorators/__init__.py 0000664 0000000 0000000 00000000001 14100726230 0026131 0 ustar 00root root 0000000 0000000
drf-extensions-0.7.1/tests_app/tests/unit/_etag/decorators/tests.py 0000664 0000000 0000000 00000067533 14100726230 0025563 0 ustar 00root root 0000000 0000000 from django.test import TestCase
from django.utils.http import quote_etag
from rest_framework import views
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import SAFE_METHODS
from rest_framework_extensions.exceptions import PreconditionRequiredException
from rest_framework_extensions.etag.decorators import (etag, api_etag)
from rest_framework.test import APIRequestFactory
from rest_framework_extensions.utils import prepare_header_name
from tests_app.testutils import (
override_extensions_api_settings,
)
factory = APIRequestFactory()
UNSAFE_METHODS = ('POST', 'PUT', 'DELETE', 'PATCH')
def dummy_api_etag_func(**kwargs):
return 'hello'
def default_etag_func(**kwargs):
return 'hello'
@override_extensions_api_settings(DEFAULT_ETAG_FUNC=default_etag_func)
class ETAGProcessorTest(TestCase):
def setUp(self):
self.request = factory.get('')
def test_should_use_etag_func_from_settings_if_it_is_not_specified(self):
etag_decorator = etag()
self.assertEqual(etag_decorator.etag_func, default_etag_func)
def test_should_add_default_etag_value(self):
class TestView(views.APIView):
@etag()
def get(self, request, *args, **kwargs):
return Response('Response from method')
view_instance = TestView()
response = view_instance.get(request=self.request)
expected_etag_value = default_etag_func()
self.assertEqual(response.get('Etag'), quote_etag(expected_etag_value))
self.assertEqual(response.data, 'Response from method')
def test_should_not_change_existing_etag_value(self):
class TestView(views.APIView):
@etag()
def get(self, request, *args, **kwargs):
return Response('Response from method', headers={'etag': 'hello'})
view_instance = TestView()
response = view_instance.get(request=self.request)
expected_etag_value = 'hello'
self.assertEqual(response.get('Etag'), expected_etag_value)
self.assertEqual(response.data, 'Response from method')
def test_should_not_change_existing_etag_value__even_if_it_is_empty(self):
class TestView(views.APIView):
@etag()
def get(self, request, *args, **kwargs):
return Response('Response from method', headers={'etag': ''})
view_instance = TestView()
response = view_instance.get(request=self.request)
expected_etag_value = ''
self.assertEqual(response.get('Etag'), expected_etag_value)
self.assertEqual(response.data, 'Response from method')
def test_should_use_custom_func_if_it_is_defined(self):
def calculate_etag(**kwargs):
return 'Custom etag'
class TestView(views.APIView):
@etag(calculate_etag)
def get(self, request, *args, **kwargs):
return Response('Response from method')
view_instance = TestView()
response = view_instance.get(request=self.request)
expected_etag_value = quote_etag('Custom etag')
self.assertEqual(response.get('Etag'), expected_etag_value)
self.assertEqual(response.data, 'Response from method')
def test_should_use_custom_method_from_view_if__etag_func__is_string(self):
used_kwargs = {}
class TestView(views.APIView):
@etag('calculate_etag')
def get(self, request, *args, **kwargs):
return Response('Response from method')
def calculate_etag(self, **kwargs):
used_kwargs.update(kwargs)
used_kwargs.update({'self': self})
return 'Custom etag'
view_instance = TestView()
response = view_instance.get(request=self.request)
expected_etag_value = quote_etag('Custom etag')
self.assertEqual(response.get('Etag'), expected_etag_value)
self.assertEqual(response.data, 'Response from method')
self.assertEqual(used_kwargs['self'], used_kwargs['view_instance'])
def test_custom_func_arguments(self):
called_with_kwargs = {}
def calculate_etag(**kwargs):
called_with_kwargs.update(kwargs)
return 'Custom etag'
class TestView(views.APIView):
@etag(calculate_etag)
def get(self, request, *args, **kwargs):
return Response('Response from method')
view_instance = TestView()
view_instance.get(self.request, 'hello', hello='world')
self.assertEqual(called_with_kwargs.get('view_instance'), view_instance)
# self.assertEqual(called_with_kwargs.get('view_method'), view_instance.get) # todo: test me
self.assertEqual(called_with_kwargs.get('args'), ('hello',))
self.assertEqual(called_with_kwargs.get('kwargs'), {'hello': 'world'})
class ETAGProcessorTestBehavior_rebuild_after_method_evaluation(TestCase):
def setUp(self):
self.request = factory.get('')
def test_should_not__rebuild_after_method_evaluation__by_default(self):
call_stack = []
def calculate_etag(**kwargs):
call_stack.append(1)
return ''.join([str(i) for i in call_stack])
class TestView(views.APIView):
@etag(calculate_etag)
def get(self, request, *args, **kwargs):
return Response('Response from method')
view_instance = TestView()
response = view_instance.get(self.request)
expected_etag_value = quote_etag('1')
self.assertEqual(response.get('Etag'), expected_etag_value)
self.assertEqual(response.data, 'Response from method')
def test_should__rebuild_after_method_evaluation__if_it_asked(self):
call_stack = []
def calculate_etag(**kwargs):
call_stack.append(1)
return ''.join([str(i) for i in call_stack])
class TestView(views.APIView):
@etag(calculate_etag, rebuild_after_method_evaluation=True)
def get(self, request, *args, **kwargs):
return Response('Response from method')
view_instance = TestView()
response = view_instance.get(self.request)
expected_etag_value = quote_etag('11')
self.assertEqual(response.get('Etag'), expected_etag_value)
self.assertEqual(response.data, 'Response from method')
class ETAGProcessorTestBehaviorMixin:
def setUp(self):
def calculate_etag(**kwargs):
return '123'
class TestView(views.APIView):
@etag(calculate_etag)
def get(self, request, *args, **kwargs):
return Response('Response from method')
self.view_instance = TestView()
self.expected_etag_value = quote_etag(calculate_etag())
def run_for_methods(self, methods, condition_failed_status):
for method in methods:
for exp in self.experiments:
headers = {
prepare_header_name(self.header_name): exp['header_value']
}
request = getattr(factory, method.lower())('', **headers)
response = self.view_instance.get(request)
base_msg = (
'For "{method}" and {header_name} value {header_value} condition should'
).format(
method=method,
header_name=self.header_name,
header_value=exp['header_value'],
)
if exp['should_fail']:
msg = base_msg + (
' fail and response must be returned with {condition_failed_status} status. '
'But it is {response_status}'
).format(condition_failed_status=condition_failed_status, response_status=response.status_code)
self.assertEqual(response.status_code, condition_failed_status, msg=msg)
msg = base_msg + ' fail and response must be empty'
self.assertEqual(response.data, None, msg=msg)
msg = (
'If precondition failed, then Etag must always be added to response. But it is {0}'
).format(response.get('Etag'))
self.assertEqual(response.get('Etag'), self.expected_etag_value, msg=msg)
else:
msg = base_msg + (
' not fail and response must be returned with 200 status. '
'But it is "{response_status}"'
).format(response_status=response.status_code)
self.assertEqual(response.status_code, status.HTTP_200_OK, msg=msg)
msg = base_msg + 'not fail and response must be filled'
self.assertEqual(response.data, 'Response from method', msg=msg)
self.assertEqual(response.get('Etag'), self.expected_etag_value, msg=msg)
class ETAGProcessorTestBehavior_if_none_match(ETAGProcessorTestBehaviorMixin, TestCase):
def setUp(self):
super().setUp()
self.header_name = 'if-none-match'
self.experiments = [
{
'header_value': '123',
'should_fail': True
},
{
'header_value': '"123"',
'should_fail': True
},
{
'header_value': '321',
'should_fail': False
},
{
'header_value': '"321"',
'should_fail': False
},
{
'header_value': '"1234"',
'should_fail': False
},
{
'header_value': '"321" "123"',
'should_fail': True
},
{
'header_value': '321 "123"',
'should_fail': True
},
{
'header_value': '*',
'should_fail': True
},
{
'header_value': '"*"',
'should_fail': True
},
{
'header_value': '321 "*"',
'should_fail': True
},
]
def test_for_safe_methods(self):
self.run_for_methods(SAFE_METHODS, condition_failed_status=status.HTTP_304_NOT_MODIFIED)
def test_for_unsafe_methods(self):
self.run_for_methods(UNSAFE_METHODS, condition_failed_status=status.HTTP_412_PRECONDITION_FAILED)
class ETAGProcessorTestBehavior_if_match(ETAGProcessorTestBehaviorMixin, TestCase):
def setUp(self):
super().setUp()
self.header_name = 'if-match'
self.experiments = [
{
'header_value': '123',
'should_fail': False
},
{
'header_value': '"123"',
'should_fail': False
},
{
'header_value': '321',
'should_fail': True
},
{
'header_value': '"321"',
'should_fail': True
},
{
'header_value': '"1234"',
'should_fail': True
},
{
'header_value': '"321" "123"',
'should_fail': False
},
{
'header_value': '321 "123"',
'should_fail': False
},
{
'header_value': '*',
'should_fail': False
},
{
'header_value': '"*"',
'should_fail': False
},
{
'header_value': '321 "*"',
'should_fail': False
},
]
def test_for_all_methods(self):
self.run_for_methods(
tuple(SAFE_METHODS) + UNSAFE_METHODS,
condition_failed_status=status.HTTP_412_PRECONDITION_FAILED
)
class APIETAGProcessorTest(TestCase):
"""Unit test cases for the APIETAGProcessor and decorator functionality."""
def setUp(self):
self.request = factory.get('')
def test_should_raise_assertion_error_if_etag_func_not_specified(self):
with self.assertRaises(AssertionError):
api_etag()
def test_should_raise_assertion_error_if_etag_func_not_specified_decorator(self):
with self.assertRaises(AssertionError):
class View(views.APIView):
@api_etag()
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
def test_should_raise_assertion_error_if_precondition_map_not_a_dict(self):
with self.assertRaises(AssertionError):
api_etag(etag_func=dummy_api_etag_func, precondition_map=['header-name'])
def test_should_raise_assertion_error_if_precondition_map_not_a_dict_decorator(self):
with self.assertRaises(AssertionError):
class View(views.APIView):
@api_etag(dummy_api_etag_func, precondition_map=['header-name'])
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
def test_should_add_object_etag_value(self):
class TestView(views.APIView):
@api_etag(dummy_api_etag_func)
def get(self, request, *args, **kwargs):
return Response('Response from GET method')
view_instance = TestView()
response = view_instance.get(request=self.request)
expected_etag_value = dummy_api_etag_func()
self.assertEqual(response.get('Etag'), quote_etag(expected_etag_value))
self.assertEqual(response.data, 'Response from GET method')
def test_should_add_object_etag_value_empty_precondition_map_decorator(self):
class TestView(views.APIView):
@api_etag(dummy_api_etag_func, precondition_map={})
def get(self, request, *args, **kwargs):
return Response('Response from GET method')
view_instance = TestView()
response = view_instance.get(request=self.request)
expected_etag_value = dummy_api_etag_func()
self.assertEqual(response.get('Etag'), quote_etag(expected_etag_value))
self.assertEqual(response.data, 'Response from GET method')
def test_should_add_object_etag_value_default_precondition_map_decorator(self):
class TestView(views.APIView):
@api_etag(dummy_api_etag_func)
def get(self, request, *args, **kwargs):
return Response('Response from GET method')
view_instance = TestView()
response = view_instance.get(request=self.request)
expected_etag_value = dummy_api_etag_func()
self.assertEqual(response.get('Etag'), quote_etag(expected_etag_value))
self.assertEqual(response.data, 'Response from GET method')
def test_should_require_precondition_decorator_unsafe_methods_empty(self):
class TestView(views.APIView):
@api_etag(dummy_api_etag_func, precondition_map={})
def put(self, request, *args, **kwargs):
return Response('Response from PUT method')
@api_etag(dummy_api_etag_func, precondition_map={})
def patch(self, request, *args, **kwargs):
return Response('Response from PATCH method')
@api_etag(dummy_api_etag_func, precondition_map={})
def delete(self, request, *args, **kwargs):
return Response('Response from DELETE method',
status=status.HTTP_204_NO_CONTENT)
view_instance = TestView()
response = view_instance.put(request=factory.put(''))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, 'Response from PUT method')
response = view_instance.patch(request=factory.patch(''))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, 'Response from PATCH method')
response = view_instance.delete(request=factory.delete(''))
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertEqual(response.data, 'Response from DELETE method')
def test_should_require_precondition_decorator_unsafe_methods_explicit(self):
class TestView(views.APIView):
@api_etag(dummy_api_etag_func, precondition_map={'PUT': ['If-Match']})
def put(self, request, *args, **kwargs):
return Response('Response from PUT method')
@api_etag(dummy_api_etag_func, precondition_map={'PATCH': ['If-Match']})
def patch(self, request, *args, **kwargs):
return Response('Response from PATCH method')
@api_etag(dummy_api_etag_func, precondition_map={'DELETE': ['If-Match']})
def delete(self, request, *args, **kwargs):
return Response('Response from DELETE method',
status=status.HTTP_204_NO_CONTENT)
view_instance = TestView()
with self.assertRaises(PreconditionRequiredException) as cm:
view_instance.put(request=factory.put(''))
self.assertEqual(cm.exception.status_code, status.HTTP_428_PRECONDITION_REQUIRED)
self.assertIsNotNone(cm.exception.detail)
with self.assertRaises(PreconditionRequiredException) as cm:
view_instance.patch(request=factory.patch(''))
self.assertEqual(cm.exception.status_code, status.HTTP_428_PRECONDITION_REQUIRED)
self.assertIsNotNone(cm.exception.detail)
with self.assertRaises(PreconditionRequiredException) as cm:
view_instance.delete(request=factory.delete(''))
self.assertEqual(cm.exception.status_code, status.HTTP_428_PRECONDITION_REQUIRED)
self.assertIsNotNone(cm.exception.detail)
def test_precondition_decorator_unsafe_methods_if_none_match(self):
def dummy_etag_func(**kwargs):
return 'some_etag'
class TestView(views.APIView):
@api_etag(dummy_etag_func)
def put(self, request, *args, **kwargs):
return Response('Response from PUT method')
@api_etag(dummy_etag_func)
def patch(self, request, *args, **kwargs):
return Response('Response from PATCH method')
@api_etag(dummy_etag_func)
def delete(self, request, *args, **kwargs):
return Response('Response from DELETE method',
status=status.HTTP_204_NO_CONTENT)
headers = {
prepare_header_name('if-none-match'): 'some_etag'
}
view_instance = TestView()
with self.assertRaises(PreconditionRequiredException) as cm:
view_instance.put(request=factory.put('', **headers))
self.assertEqual(cm.exception.status_code, status.HTTP_428_PRECONDITION_REQUIRED)
self.assertIsNotNone(cm.exception.detail)
with self.assertRaises(PreconditionRequiredException) as cm:
view_instance.patch(request=factory.patch('', **headers))
self.assertEqual(cm.exception.status_code, status.HTTP_428_PRECONDITION_REQUIRED)
self.assertIsNotNone(cm.exception.detail)
with self.assertRaises(PreconditionRequiredException) as cm:
view_instance.delete(request=factory.delete('', **headers))
self.assertEqual(cm.exception.status_code, status.HTTP_428_PRECONDITION_REQUIRED)
self.assertIsNotNone(cm.exception.detail)
def test_should_require_precondition_decorator_unsafe_methods_default(self):
class TestView(views.APIView):
@api_etag(dummy_api_etag_func)
def put(self, request, *args, **kwargs):
return Response('Response from PUT method')
@api_etag(dummy_api_etag_func)
def patch(self, request, *args, **kwargs):
return Response('Response from PATCH method')
@api_etag(dummy_api_etag_func)
def delete(self, request, *args, **kwargs):
return Response('Response from DELETE method',
status=status.HTTP_204_NO_CONTENT)
view_instance = TestView()
with self.assertRaises(PreconditionRequiredException) as cm:
view_instance.put(request=factory.put(''))
self.assertEqual(cm.exception.status_code, status.HTTP_428_PRECONDITION_REQUIRED)
self.assertIsNotNone(cm.exception.detail)
with self.assertRaises(PreconditionRequiredException) as cm:
view_instance.patch(request=factory.patch(''))
self.assertEqual(cm.exception.status_code, status.HTTP_428_PRECONDITION_REQUIRED)
self.assertIsNotNone(cm.exception.detail)
with self.assertRaises(PreconditionRequiredException) as cm:
view_instance.delete(request=factory.delete(''))
self.assertEqual(cm.exception.status_code, status.HTTP_428_PRECONDITION_REQUIRED)
self.assertIsNotNone(cm.exception.detail)
class APIETAGProcessorTestBehaviorMixin:
def setUp(self):
def calculate_etag(**kwargs):
return '123'
class TestView(views.APIView):
@api_etag(calculate_etag)
def head(self, request, *args, **kwargs):
return Response('Response from HEAD method')
@api_etag(calculate_etag)
def options(self, request, *args, **kwargs):
return Response('Response from OPTIONS method')
@api_etag(calculate_etag, precondition_map={})
def post(self, request, *args, **kwargs):
return Response('Response from POST method',
status=status.HTTP_201_CREATED)
@api_etag(calculate_etag)
def get(self, request, *args, **kwargs):
return Response('Response from GET method')
@api_etag(calculate_etag)
def put(self, request, *args, **kwargs):
return Response('Response from PUT method')
@api_etag(calculate_etag)
def patch(self, request, *args, **kwargs):
return Response('Response from PATCH method')
@api_etag(calculate_etag)
def delete(self, request, *args, **kwargs):
return Response('Response from DELETE method',
status=status.HTTP_204_NO_CONTENT)
self.view_instance = TestView()
self.expected_etag_value = quote_etag(calculate_etag())
def run_for_methods(self, methods, condition_failed_status, experiments=None):
for method in methods:
if experiments is None:
experiments = self.experiments
for exp in experiments:
headers = {
prepare_header_name(self.header_name): exp['header_value']
}
request = getattr(factory, method.lower())('', **headers)
response = getattr(self.view_instance, method.lower())(request)
base_msg = (
'For "{method}" and {header_name} value {header_value} condition should'
).format(
method=method,
header_name=self.header_name,
header_value=exp['header_value'],
)
if exp['should_fail']:
msg = base_msg + (
' fail and response must be returned with {condition_failed_status} status. '
'But it is {response_status}'
).format(condition_failed_status=condition_failed_status, response_status=response.status_code)
self.assertEqual(response.status_code, condition_failed_status, msg=msg)
msg = base_msg + ' fail and response must be empty'
self.assertEqual(response.data, None, msg=msg)
msg = (
'If precondition failed, then Etag must always be added to response. But it is {0}'
).format(response.get('Etag'))
self.assertEqual(response.get('Etag'), self.expected_etag_value, msg=msg)
else:
if method.lower() == 'delete':
success_status = status.HTTP_204_NO_CONTENT
elif method.lower() == 'post':
success_status = status.HTTP_201_CREATED
else:
success_status = status.HTTP_200_OK
msg = base_msg + (
' not fail and response must be returned with %s status. '
'But it is "{response_status}"'
).format(success_status, response_status=response.status_code)
self.assertEqual(response.status_code, success_status, msg=msg)
msg = base_msg + 'not fail and response must be filled'
self.assertEqual(response.data, 'Response from %s method' % method.upper(), msg=msg)
self.assertEqual(response.get('Etag'), self.expected_etag_value, msg=msg)
class APIETAGProcessorTestBehavior_if_match(APIETAGProcessorTestBehaviorMixin, TestCase):
def setUp(self):
super().setUp()
self.header_name = 'if-match'
self.experiments = [
{
'header_value': '123',
'should_fail': False
},
{
'header_value': '"123"',
'should_fail': False
},
{
'header_value': '321',
'should_fail': True
},
{
'header_value': '"321"',
'should_fail': True
},
{
'header_value': '"1234"',
'should_fail': True
},
{
'header_value': '"321" "123"',
'should_fail': False
},
{
'header_value': '321 "123"',
'should_fail': False
},
{
'header_value': '*',
'should_fail': False
},
{
'header_value': '"*"',
'should_fail': False
},
{
'header_value': '321 "*"',
'should_fail': False
},
]
def test_for_all_methods(self):
self.run_for_methods(
tuple(SAFE_METHODS) + UNSAFE_METHODS,
condition_failed_status=status.HTTP_412_PRECONDITION_FAILED
)
class APIETAGProcessorTestBehavior_if_none_match(APIETAGProcessorTestBehaviorMixin, TestCase):
def setUp(self):
super().setUp()
self.header_name = 'if-none-match'
self.experiments = [
{
'header_value': '123',
'should_fail': True
},
{
'header_value': '"123"',
'should_fail': True
},
{
'header_value': '321',
'should_fail': False
},
{
'header_value': '"321"',
'should_fail': False
},
{
'header_value': '"1234"',
'should_fail': False
},
{
'header_value': '"321" "123"',
'should_fail': True
},
{
'header_value': '321 "123"',
'should_fail': True
},
{
'header_value': '*',
'should_fail': True
},
{
'header_value': '"*"',
'should_fail': True
},
{
'header_value': '321 "*"',
'should_fail': True
},
]
def test_for_safe_methods(self):
self.run_for_methods(SAFE_METHODS, condition_failed_status=status.HTTP_304_NOT_MODIFIED)
# NB: We don't test the unsafe methods here, since the PreconditionRequiredException would require us to hack the
# runner method. However, we tested the exceptions in the APIETAGProcessorTest class.
drf-extensions-0.7.1/tests_app/tests/unit/cache/ 0000775 0000000 0000000 00000000000 14100726230 0021650 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/unit/cache/__init__.py 0000664 0000000 0000000 00000000000 14100726230 0023747 0 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/unit/cache/decorators/ 0000775 0000000 0000000 00000000000 14100726230 0024015 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/unit/cache/decorators/__init__.py 0000664 0000000 0000000 00000000001 14100726230 0026115 0 ustar 00root root 0000000 0000000
drf-extensions-0.7.1/tests_app/tests/unit/cache/decorators/tests.py 0000664 0000000 0000000 00000032753 14100726230 0025543 0 ustar 00root root 0000000 0000000 from django.core.cache import caches
from django.test import TestCase
from mock import Mock, patch
from rest_framework import views
from rest_framework.response import Response
from rest_framework_extensions.cache.decorators import cache_response
from rest_framework_extensions.settings import extensions_api_settings
from rest_framework.test import APIRequestFactory
from tests_app.testutils import override_extensions_api_settings
factory = APIRequestFactory()
class CacheResponseTest(TestCase):
def setUp(self):
super().setUp()
self.request = factory.get('')
self.cache = caches[extensions_api_settings.DEFAULT_USE_CACHE]
def test_should_return_response_if_it_is_not_in_cache(self):
class TestView(views.APIView):
@cache_response()
def get(self, request, *args, **kwargs):
return Response('Response from method 1')
view_instance = TestView()
response = view_instance.dispatch(request=self.request)
self.assertEqual(response.data, 'Response from method 1')
self.assertEqual(type(response), Response)
@override_extensions_api_settings(DEFAULT_CACHE_KEY_FUNC=Mock(return_value='cache_response_key'))
def test_should_store_response_in_cache_by_key_function_which_specified_in_settings(self):
class TestView(views.APIView):
@cache_response()
def get(self, request, *args, **kwargs):
return Response('Response from method 2')
view_instance = TestView()
response = view_instance.dispatch(request=self.request)
self.assertEqual(self.cache.get('cache_response_key')
[0], response.content)
self.assertEqual(type(response), Response)
def test_should_store_response_in_cache_by_key_function_which_specified_in_arguments(self):
def key_func(*args, **kwargs):
return 'cache_response_key_from_func'
class TestView(views.APIView):
@cache_response(key_func=key_func)
def get(self, request, *args, **kwargs):
return Response('Response from method 3')
view_instance = TestView()
response = view_instance.dispatch(request=self.request)
self.assertEqual(self.cache.get(
'cache_response_key_from_func')[0], response.content)
self.assertEqual(type(response), Response)
def test_should_store_response_in_cache_by_key_which_calculated_by_view_method__if__key_func__is_string(self):
class TestView(views.APIView):
@cache_response(key_func='key_func')
def get(self, request, *args, **kwargs):
return Response('Response from method 3')
def key_func(self, *args, **kwargs):
return 'cache_response_key_from_method'
view_instance = TestView()
response = view_instance.dispatch(request=self.request)
self.assertEqual(self.cache.get(
'cache_response_key_from_method')[0], response.content)
self.assertEqual(type(response), Response)
def test_key_func_call_arguments(self):
called_with_kwargs = {}
def key_func(**kwargs):
called_with_kwargs.update(kwargs)
return 'cache_response_key_from_func'
class TestView(views.APIView):
@cache_response(key_func=key_func)
def get(self, request, *args, **kwargs):
return Response('Response from method 3')
view_instance = TestView()
response = view_instance.dispatch(self.request, 'hello', hello='world')
self.assertEqual(called_with_kwargs.get(
'view_instance'), view_instance)
# self.assertEqual(called_with_kwargs.get('view_method'), view_instance.get) # todo: test me
self.assertEqual(called_with_kwargs.get('args'), ('hello',))
self.assertEqual(called_with_kwargs.get('kwargs'), {'hello': 'world'})
@override_extensions_api_settings(
DEFAULT_CACHE_RESPONSE_TIMEOUT=100,
DEFAULT_CACHE_KEY_FUNC=Mock(return_value='cache_response_key')
)
def test_should_store_response_in_cache_with_timeout_from_settings(self):
cache_response_decorator = cache_response()
cache_response_decorator.cache.set = Mock()
class TestView(views.APIView):
@cache_response_decorator
def get(self, request, *args, **kwargs):
return Response('Response from method 4')
view_instance = TestView()
response = view_instance.dispatch(request=self.request)
self.assertTrue(
cache_response_decorator.cache.set.called,
'Cache saving should be performed')
self.assertEqual(
cache_response_decorator.cache.set.call_args_list[0][0][2], 100)
def test_should_store_response_in_cache_with_timeout_from_arguments(self):
cache_response_decorator = cache_response(timeout=3)
cache_response_decorator.cache.set = Mock()
class TestView(views.APIView):
@cache_response_decorator
def get(self, request, *args, **kwargs):
return Response('Response from method 4')
view_instance = TestView()
response = view_instance.dispatch(request=self.request)
self.assertTrue(
cache_response_decorator.cache.set.called,
'Cache saving should be performed')
self.assertEqual(
cache_response_decorator.cache.set.call_args_list[0][0][2], 3)
def test_should_store_response_in_cache_with_timeout_from_object_cache_timeout_property(self):
cache_response_decorator = cache_response(
timeout='object_cache_timeout')
cache_response_decorator.cache.set = Mock()
class TestView(views.APIView):
object_cache_timeout = 20
@cache_response_decorator
def get(self, request, *args, **kwargs):
return Response('Response from method 4')
view_instance = TestView()
response = view_instance.dispatch(request=self.request)
self.assertTrue(
cache_response_decorator.cache.set.called,
'Cache saving should be performed')
self.assertEqual(
cache_response_decorator.cache.set.call_args_list[0][0][2], 20)
def test_should_store_response_in_cache_with_timeout_from_list_cache_timeout_property(self):
cache_response_decorator = cache_response(timeout='list_cache_timeout')
cache_response_decorator.cache.set = Mock()
class TestView(views.APIView):
list_cache_timeout = 10
@cache_response_decorator
def get(self, request, *args, **kwargs):
return Response('Response from method 4')
view_instance = TestView()
response = view_instance.dispatch(request=self.request)
self.assertTrue(
cache_response_decorator.cache.set.called,
'Cache saving should be performed')
self.assertEqual(
cache_response_decorator.cache.set.call_args_list[0][0][2], 10)
def test_should_return_response_from_cache_if_it_is_in_it(self):
def key_func(**kwargs):
return 'cache_response_key'
class TestView(views.APIView):
@cache_response(key_func=key_func)
def get(self, request, *args, **kwargs):
return Response(u'Response from method 4')
view_instance = TestView()
view_instance.headers = {}
cached_response = Response('Cached response from method 4')
view_instance.finalize_response(
request=self.request, response=cached_response)
cached_response.render()
# django 3.0 has not .items() method, django 3.2 has not ._headers
if hasattr(cached_response, '_headers'):
headers = cached_response._headers
else:
headers = {k: (k, v) for k, v in cached_response.items()}
response_dict = (
cached_response.rendered_content,
cached_response.status_code,
headers
)
self.cache.set('cache_response_key', response_dict)
response = view_instance.dispatch(request=self.request)
self.assertEqual(
response.content.decode('utf-8'),
'"Cached response from method 4"')
@override_extensions_api_settings(
DEFAULT_USE_CACHE='special_cache'
)
def test_should_use_cache_from_settings_by_default(self):
def key_func(**kwargs):
return 'cache_response_key'
class TestView(views.APIView):
@cache_response(key_func=key_func)
def get(self, request, *args, **kwargs):
return Response(u'Response from method 5')
view_instance = TestView()
view_instance.dispatch(request=self.request)
data_from_cache = caches['special_cache'].get('cache_response_key')
self.assertEqual(len(data_from_cache), 3)
self.assertEqual(
data_from_cache[0].decode('utf-8'),
u'"Response from method 5"')
@override_extensions_api_settings(
DEFAULT_USE_CACHE='special_cache'
)
def test_should_use_cache_from_decorator_if_it_is_specified(self):
def key_func(**kwargs):
return 'cache_response_key'
class TestView(views.APIView):
@cache_response(key_func=key_func, cache='another_special_cache')
def get(self, request, *args, **kwargs):
return Response(u'Response from method 6')
view_instance = TestView()
view_instance.dispatch(request=self.request)
data_from_cache = caches['another_special_cache'].get(
'cache_response_key')
self.assertEqual(len(data_from_cache), 3)
self.assertEqual(data_from_cache[0].decode(
'utf-8'), u'"Response from method 6"')
def test_should_reuse_cache_singleton(self):
"""
https://github.com/chibisov/drf-extensions/issues/26
https://docs.djangoproject.com/en/dev/topics/cache/#django.core.cache.caches
"""
cache_response_instance = cache_response()
another_cache_response_instance = cache_response()
self.assertTrue(
cache_response_instance.cache is another_cache_response_instance.cache)
def test_dont_cache_response_with_error_if_cache_error_false(self):
cache_response_decorator = cache_response(cache_errors=False)
class TestView(views.APIView):
def __init__(self, status, *args, **kwargs):
self.status = status
super().__init__(*args, **kwargs)
@cache_response_decorator
def get(self, request, *args, **kwargs):
return Response(status=self.status)
with patch.object(cache_response_decorator.cache, 'set'):
for status in (400, 500):
view_instance = TestView(status=status)
view_instance.dispatch(request=self.request)
self.assertFalse(cache_response_decorator.cache.set.called)
def test_cache_response_with_error_by_default(self):
cache_response_decorator = cache_response()
class TestView(views.APIView):
def __init__(self, status, *args, **kwargs):
self.status = status
super().__init__(*args, **kwargs)
@cache_response_decorator
def get(self, request, *args, **kwargs):
return Response(status=self.status)
with patch.object(cache_response_decorator.cache, 'set'):
for status in (400, 500):
view_instance = TestView(status=status)
view_instance.dispatch(request=self.request)
self.assertTrue(cache_response_decorator.cache.set.called)
@override_extensions_api_settings(
DEFAULT_CACHE_ERRORS=False
)
def test_should_use_cache_error_from_settings_by_default(self):
self.assertFalse(cache_response().cache_errors)
@override_extensions_api_settings(
DEFAULT_CACHE_ERRORS=False
)
def test_should_use_cache_error_from_decorator_if_it_is_specified(self):
self.assertTrue(cache_response(cache_errors=True).cache_errors)
def test_should_return_response_with_tuple_headers(self):
def key_func(**kwargs):
return 'cache_response_key'
class TestView(views.APIView):
@cache_response(key_func=key_func)
def get(self, request, *args, **kwargs):
return Response(u'')
view_instance = TestView()
view_instance.headers = {'Test': 'foo'}
cached_response = Response(u'')
view_instance.finalize_response(
request=self.request, response=cached_response)
cached_response.render()
# django 3.0 has not .items() method, django 3.2 has not ._headers
if hasattr(cached_response, '_headers'):
headers = {k: list(v) for k, v in cached_response._headers.items()}
else:
headers = {k: (k, v) for k, v in cached_response.items()}
response_dict = (
cached_response.rendered_content,
cached_response.status_code,
headers
)
self.cache.set('cache_response_key', response_dict)
response = view_instance.dispatch(request=self.request)
# django 3.0 has not .items() method, django 3.2 has not ._headers
if hasattr(response, '_headers'):
self.assertTrue(all(isinstance(v, tuple)
for v in response._headers.values()))
self.assertEqual(response._headers['test'], ('Test', 'foo'))
else:
self.assertEqual(response['test'], 'foo')
drf-extensions-0.7.1/tests_app/tests/unit/decorators/ 0000775 0000000 0000000 00000000000 14100726230 0022752 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/unit/decorators/__init__.py 0000664 0000000 0000000 00000000000 14100726230 0025051 0 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/unit/decorators/tests.py 0000664 0000000 0000000 00000003001 14100726230 0024460 0 ustar 00root root 0000000 0000000 from django.test import TestCase
from rest_framework import pagination, viewsets
from rest_framework_extensions.decorators import paginate
class TestPaginateDecorator(TestCase):
def test_empty_pagination_class(self):
msg = "@paginate missing required argument: 'pagination_class'"
with self.assertRaisesMessage(AssertionError, msg):
@paginate()
class MockGenericViewSet(viewsets.GenericViewSet):
pass
def test_adding_page_number_pagination(self):
"""
Other default pagination classes' test result will be same as this even if kwargs changed to anything.
"""
@paginate(pagination_class=pagination.PageNumberPagination, page_size=5, ordering='-created_at')
class MockGenericViewSet(viewsets.GenericViewSet):
pass
assert hasattr(MockGenericViewSet, 'pagination_class')
assert MockGenericViewSet.pagination_class().page_size == 5
assert MockGenericViewSet.pagination_class().ordering == '-created_at'
def test_adding_custom_pagination(self):
class CustomPagination(pagination.BasePagination):
pass
@paginate(pagination_class=CustomPagination, kwarg1='kwarg1', kwarg2='kwarg2')
class MockGenericViewSet(viewsets.GenericViewSet):
pass
assert hasattr(MockGenericViewSet, 'pagination_class')
assert MockGenericViewSet.pagination_class().kwarg1 == 'kwarg1'
assert MockGenericViewSet.pagination_class().kwarg2 == 'kwarg2'
drf-extensions-0.7.1/tests_app/tests/unit/key_constructor/ 0000775 0000000 0000000 00000000000 14100726230 0024042 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/unit/key_constructor/__init__.py 0000664 0000000 0000000 00000000001 14100726230 0026142 0 ustar 00root root 0000000 0000000
drf-extensions-0.7.1/tests_app/tests/unit/key_constructor/bits/ 0000775 0000000 0000000 00000000000 14100726230 0025003 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/unit/key_constructor/bits/__init__.py 0000664 0000000 0000000 00000000001 14100726230 0027103 0 ustar 00root root 0000000 0000000
drf-extensions-0.7.1/tests_app/tests/unit/key_constructor/bits/models.py 0000664 0000000 0000000 00000000164 14100726230 0026641 0 ustar 00root root 0000000 0000000 from django.db import models
class BitTestModel(models.Model):
is_active = models.BooleanField(default=False)
drf-extensions-0.7.1/tests_app/tests/unit/key_constructor/bits/tests.py 0000664 0000000 0000000 00000053140 14100726230 0026522 0 ustar 00root root 0000000 0000000 from mock import Mock
from mock import PropertyMock
import django
from django.test import TestCase
from django.utils.translation import override
from rest_framework import views
from rest_framework.response import Response
from rest_framework.test import APIRequestFactory
from rest_framework_extensions.key_constructor.bits import (
KeyBitDictBase,
UniqueMethodIdKeyBit,
LanguageKeyBit,
FormatKeyBit,
UserKeyBit,
HeadersKeyBit,
RequestMetaKeyBit,
QueryParamsKeyBit,
UniqueViewIdKeyBit,
PaginationKeyBit,
ListSqlQueryKeyBit,
RetrieveSqlQueryKeyBit,
ListModelKeyBit,
RetrieveModelKeyBit,
ArgsKeyBit,
KwargsKeyBit,
)
from .models import BitTestModel
factory = APIRequestFactory()
class KeyBitDictBaseTest(TestCase):
def setUp(self):
self.kwargs = {
'params': [],
'view_instance': None,
'view_method': None,
'request': None,
'args': None,
'kwargs': None
}
def test_should_raise_exception_if__get_source_dict__is_not_implemented(self):
class KeyBitDictChild(KeyBitDictBase):
pass
try:
KeyBitDictChild().get_data(**self.kwargs)
except NotImplementedError:
pass
else:
self.fail('Should raise NotImplementedError if "get_source_dict" method is not implemented')
def test_should_return_empty_dict_if_source_dict_is_empty(self):
class KeyBitDictChild(KeyBitDictBase):
def get_source_dict(self, **kwargs):
return {}
self.assertEqual(KeyBitDictChild().get_data(**self.kwargs), {})
def test_should_retrieve_data_by_keys_from_params_list_from_source_dict(self):
class KeyBitDictChild(KeyBitDictBase):
def get_source_dict(self, **kwargs):
return {
'id': 1,
'geobase_id': 123,
'name': 'London',
}
self.kwargs['params'] = ['name', 'geobase_id']
expected = {
'name': u'London',
'geobase_id': u'123',
}
self.assertEqual(KeyBitDictChild().get_data(**self.kwargs), expected)
def test_should_not_retrieve_data_with_none_value(self):
class KeyBitDictChild(KeyBitDictBase):
def get_source_dict(self, **kwargs):
return {
'id': 1,
'geobase_id': 123,
'name': None,
}
self.kwargs['params'] = ['name', 'geobase_id']
expected = {
'geobase_id': u'123',
}
self.assertEqual(KeyBitDictChild().get_data(**self.kwargs), expected)
def test_should_force_text_for_value(self):
class KeyBitDictChild(KeyBitDictBase):
def get_source_dict(self, **kwargs):
return {
'id': 1,
'geobase_id': 123,
'name': 'Лондон',
}
self.kwargs['params'] = ['name', 'geobase_id']
expected = {
'geobase_id': u'123',
'name': u'Лондон',
}
self.assertEqual(KeyBitDictChild().get_data(**self.kwargs), expected)
def test_should_prepare_key_before_retrieving(self):
class KeyBitDictChild(KeyBitDictBase):
def get_source_dict(self, **kwargs):
return {
'id': 1,
'GEOBASE_ID': 123,
'NAME': 'London',
}
def prepare_key_for_value_retrieving(self, key):
return key.upper()
self.kwargs['params'] = ['name', 'geobase_id']
expected = {
'geobase_id': u'123',
'name': u'London',
}
self.assertEqual(KeyBitDictChild().get_data(**self.kwargs), expected)
def test_should_prepare_key_before_value_assignment(self):
class KeyBitDictChild(KeyBitDictBase):
def get_source_dict(self, **kwargs):
return {
'id': 1,
'geobase_id': 123,
'name': 'London',
}
def prepare_key_for_value_assignment(self, key):
return key.upper()
self.kwargs['params'] = ['name', 'geobase_id']
expected = {
'GEOBASE_ID': u'123',
'NAME': u'London',
}
self.assertEqual(KeyBitDictChild().get_data(**self.kwargs), expected)
def test_should_produce_exact_results_for_equal_params_attribute_with_different_items_ordering(self):
class KeyBitDictChild(KeyBitDictBase):
def get_source_dict(self, **kwargs):
return {
'id': 1,
'GEOBASE_ID': 123,
'NAME': 'London',
}
self.kwargs['params'] = ['name', 'geobase_id']
response_1 = KeyBitDictChild().get_data(**self.kwargs)
self.kwargs['params'] = ['geobase_id', 'name']
response_2 = KeyBitDictChild().get_data(**self.kwargs)
self.assertEqual(response_1, response_2)
class UniqueViewIdKeyBitTest(TestCase):
def test_resulting_dict(self):
class TestView(views.APIView):
def get(self, request, *args, **kwargs):
return Response('Response from method')
view_instance = TestView()
kwargs = {
'params': None,
'view_instance': view_instance,
'view_method': view_instance.get,
'request': None,
'args': None,
'kwargs': None
}
expected = u'tests_app.tests.unit.key_constructor.bits.tests' + u'.' + u'TestView'
self.assertEqual(UniqueViewIdKeyBit().get_data(**kwargs), expected)
class UniqueMethodIdKeyBitTest(TestCase):
def test_resulting_dict(self):
class TestView(views.APIView):
def get(self, request, *args, **kwargs):
return Response('Response from method')
view_instance = TestView()
kwargs = {
'params': None,
'view_instance': view_instance,
'view_method': view_instance.get,
'request': None,
'args': None,
'kwargs': None
}
expected = u'tests_app.tests.unit.key_constructor.bits.tests' + u'.' + u'TestView' + u'.' + u'get'
self.assertEqual(UniqueMethodIdKeyBit().get_data(**kwargs), expected)
class LanguageKeyBitTest(TestCase):
def test_resulting_dict(self):
kwargs = {
'params': None,
'view_instance': None,
'view_method': None,
'request': None,
'args': None,
'kwargs': None
}
expected = u'br'
with override('br'):
self.assertEqual(LanguageKeyBit().get_data(**kwargs), expected)
class FormatKeyBitTest(TestCase):
def test_resulting_dict(self):
kwargs = {
'params': None,
'view_instance': None,
'view_method': None,
'request': factory.get(''),
'args': None,
'kwargs': None
}
kwargs['request'].accepted_renderer = Mock(format='super-format')
expected = u'super-format'
self.assertEqual(FormatKeyBit().get_data(**kwargs), expected)
class UserKeyBitTest(TestCase):
def setUp(self):
self.kwargs = {
'params': None,
'view_instance': None,
'view_method': None,
'request': factory.get(''),
'args': None,
'kwargs': None
}
self.user = Mock()
self.user.id = 123
self.is_authenticated = PropertyMock(return_value=False)
type(self.user).is_authenticated = self.is_authenticated
def test_without_user_in_request(self):
expected = u'anonymous'
self.assertEqual(UserKeyBit().get_data(**self.kwargs), expected)
def test_with_not_autenticated_user(self):
self.kwargs['request'].user = self.user
expected = u'anonymous'
self.assertEqual(UserKeyBit().get_data(**self.kwargs), expected)
def test_with_autenticated_user(self):
self.kwargs['request'].user = self.user
self.is_authenticated.return_value = True
expected = u'123'
self.assertEqual(UserKeyBit().get_data(**self.kwargs), expected)
class HeadersKeyBitTest(TestCase):
def test_resulting_dict(self):
self.kwargs = {
'params': ['Accept-Language', 'X-Geobase-Id', 'Not-Existing-Header'],
'view_instance': None,
'view_method': None,
'request': factory.get('', **{
'HTTP_ACCEPT_LANGUAGE': 'Ru',
'HTTP_X_GEOBASE_ID': 123
}),
'args': None,
'kwargs': None
}
expected = {
'accept-language': u'Ru',
'x-geobase-id': u'123'
}
self.assertEqual(HeadersKeyBit().get_data(**self.kwargs), expected)
class RequestMetaKeyBitTest(TestCase):
def test_resulting_dict(self):
self.kwargs = {
'params': ['REMOTE_ADDR', 'REMOTE_HOST', 'not_existing_key'],
'view_instance': None,
'view_method': None,
'request': factory.get('', **{
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_HOST': 'localhost'
}),
'args': None,
'kwargs': None
}
expected = {
'REMOTE_ADDR': u'127.0.0.1',
'REMOTE_HOST': u'localhost'
}
self.assertEqual(RequestMetaKeyBit().get_data(**self.kwargs), expected)
class QueryParamsKeyBitTest(TestCase):
def setUp(self):
self.kwargs = {
'params': None,
'view_instance': None,
'view_method': None,
'request': factory.get('?part=Londo&callback=jquery_callback'),
'args': None,
'kwargs': None
}
def test_resulting_dict(self):
self.kwargs['params'] = ['part', 'callback', 'not_existing_param']
expected = {
'part': u'Londo',
'callback': u'jquery_callback'
}
self.assertEqual(QueryParamsKeyBit().get_data(**self.kwargs), expected)
def test_resulting_dict_all_params(self):
self.kwargs['params'] = '*'
expected = {
'part': u'Londo',
'callback': u'jquery_callback'
}
self.assertEqual(QueryParamsKeyBit().get_data(**self.kwargs), expected)
def test_default_params_is_all_args(self):
self.assertEqual(QueryParamsKeyBit().params, '*')
class PaginationKeyBitTest(TestCase):
def setUp(self):
self.kwargs = {
'params': None,
'view_instance': Mock(spec_set=['paginator']),
'view_method': None,
'request': factory.get('?page_size=10&page=1&limit=5&offset=15&cursor=foo'),
'args': None,
'kwargs': None
}
def test_view_without_pagination_arguments(self):
self.kwargs['view_instance'] = Mock(spec_set=[])
self.assertEqual(PaginationKeyBit().get_data(**self.kwargs), {})
def test_view_with_empty_pagination_arguments(self):
self.kwargs['view_instance'].paginator.page_query_param = None
self.kwargs['view_instance'].paginator.page_size_query_param = None
self.assertEqual(PaginationKeyBit().get_data(**self.kwargs), {})
def test_view_with_page_kwarg(self):
self.kwargs['view_instance'].paginator.page_query_param = 'page'
self.kwargs['view_instance'].paginator.page_size_query_param = None
self.assertEqual(PaginationKeyBit().get_data(**self.kwargs), {'page': '1'})
def test_view_with_paginate_by_param(self):
self.kwargs['view_instance'].paginator.page_query_param = None
self.kwargs['view_instance'].paginator.page_size_query_param = 'page_size'
self.assertEqual(PaginationKeyBit().get_data(**self.kwargs), {'page_size': '10'})
def test_view_with_all_pagination_attrs(self):
self.kwargs['view_instance'].paginator.page_query_param = 'page'
self.kwargs['view_instance'].paginator.page_size_query_param = 'page_size'
self.assertEqual(PaginationKeyBit().get_data(**self.kwargs), {'page_size': '10', 'page': '1'})
def test_view_with_all_pagination_attrs__without_query_params(self):
self.kwargs['view_instance'].paginator.page_query_param = 'page'
self.kwargs['view_instance'].paginator.page_size_query_param = 'page_size'
self.kwargs['request'] = factory.get('')
self.assertEqual(PaginationKeyBit().get_data(**self.kwargs), {})
def test_view_with_offset_pagination_attrs(self):
self.kwargs['view_instance'].paginator.limit_query_param = 'limit'
self.kwargs['view_instance'].paginator.offset_query_param = 'offset'
self.assertEqual(PaginationKeyBit().get_data(**self.kwargs), {'limit': '5', 'offset': '15'})
def test_view_with_cursor_pagination_attrs(self):
self.kwargs['view_instance'].paginator.cursor_query_param = 'cursor'
self.assertEqual(PaginationKeyBit().get_data(**self.kwargs), {'cursor': 'foo'})
class ListSqlQueryKeyBitTest(TestCase):
def setUp(self):
self.kwargs = {
'params': None,
'view_instance': Mock(),
'view_method': None,
'request': None,
'args': None,
'kwargs': None
}
self.kwargs['view_instance'].get_queryset = Mock(return_value=BitTestModel.objects.all())
self.kwargs['view_instance'].filter_queryset = lambda x: x.filter(is_active=True)
def test_should_use_view__get_queryset__and_filter_it_with__filter_queryset(self):
if django.VERSION >= (3, 1):
expected = ('SELECT "unit_bittestmodel"."id", "unit_bittestmodel"."is_active" '
'FROM "unit_bittestmodel" '
'WHERE "unit_bittestmodel"."is_active"')
else:
expected = ('SELECT "unit_bittestmodel"."id", "unit_bittestmodel"."is_active" '
'FROM "unit_bittestmodel" '
'WHERE "unit_bittestmodel"."is_active" = True')
response = ListSqlQueryKeyBit().get_data(**self.kwargs)
self.assertEqual(response, expected)
def test_should_return_none_if_empty_queryset(self):
self.kwargs['view_instance'].filter_queryset = lambda x: x.none()
response = ListSqlQueryKeyBit().get_data(**self.kwargs)
self.assertEqual(response, None)
def test_should_return_none_if_empty_result_set_raised(self):
self.kwargs['view_instance'].filter_queryset = lambda x: x.filter(pk__in=[])
response = ListSqlQueryKeyBit().get_data(**self.kwargs)
self.assertEqual(response, None)
class ListModelKeyBitTest(TestCase):
def setUp(self):
self.kwargs = {
'params': None,
'view_instance': Mock(),
'view_method': None,
'request': None,
'args': None,
'kwargs': None
}
self.kwargs['view_instance'].get_queryset = Mock(return_value=BitTestModel.objects.all())
self.kwargs['view_instance'].filter_queryset = lambda x: x.filter(is_active=True)
def test_should_use_view__get_queryset__and_filter_it_with__filter_queryset(self):
# create 4 models
BitTestModel.objects.create(is_active=True)
BitTestModel.objects.create(is_active=True)
BitTestModel.objects.create(is_active=True)
BitTestModel.objects.create(is_active=True)
expected = u"[(1, True), (2, True), (3, True), (4, True)]"
response = ListModelKeyBit().get_data(**self.kwargs)
self.assertEqual(response, expected)
def test_should_return_none_if_empty_queryset(self):
self.kwargs['view_instance'].filter_queryset = lambda x: x.none()
response = ListModelKeyBit().get_data(**self.kwargs)
self.assertEqual(response, None)
def test_should_return_none_if_empty_result_set_raised(self):
self.kwargs['view_instance'].filter_queryset = lambda x: x.filter(pk__in=[])
response = ListModelKeyBit().get_data(**self.kwargs)
self.assertEqual(response, None)
class RetrieveSqlQueryKeyBitTest(TestCase):
def setUp(self):
self.kwargs = {
'params': None,
'view_instance': Mock(),
'view_method': None,
'request': None,
'args': None,
'kwargs': None
}
self.kwargs['view_instance'].kwargs = {'id': 123}
self.kwargs['view_instance'].lookup_field = 'id'
self.kwargs['view_instance'].get_queryset = Mock(return_value=BitTestModel.objects.all())
self.kwargs['view_instance'].filter_queryset = lambda x: x.filter(is_active=True)
def test_should_use_view__get_queryset__and_filter_it_with__filter_queryset__and_filter_by__lookup_field(self):
if django.VERSION >= (3, 1):
expected = ('SELECT "unit_bittestmodel"."id", "unit_bittestmodel"."is_active" '
'FROM "unit_bittestmodel" '
'WHERE ("unit_bittestmodel"."is_active" AND "unit_bittestmodel"."id" = 123)')
else:
expected = ('SELECT "unit_bittestmodel"."id", "unit_bittestmodel"."is_active" '
'FROM "unit_bittestmodel" '
'WHERE ("unit_bittestmodel"."is_active" = True AND "unit_bittestmodel"."id" = 123)')
response = RetrieveSqlQueryKeyBit().get_data(**self.kwargs)
self.assertEqual(response, expected)
def test_with_bad_lookup_value(self):
self.kwargs['view_instance'].kwargs = {'id': "I'm ganna hack u are!"}
response = RetrieveSqlQueryKeyBit().get_data(**self.kwargs)
self.assertEqual(response, None)
def test_should_return_none_if_empty_queryset(self):
self.kwargs['view_instance'].filter_queryset = lambda x: x.none()
response = RetrieveSqlQueryKeyBit().get_data(**self.kwargs)
self.assertEqual(response, None)
def test_should_return_none_if_empty_result_set_raised(self):
self.kwargs['view_instance'].filter_queryset = lambda x: x.filter(pk__in=[])
response = RetrieveSqlQueryKeyBit().get_data(**self.kwargs)
self.assertEqual(response, None)
class RetrieveModelKeyBitTest(TestCase):
def setUp(self):
self.kwargs = {
'params': None,
'view_instance': Mock(),
'view_method': None,
'request': None,
'args': None,
'kwargs': None
}
self.kwargs['view_instance'].kwargs = {'id': 123}
self.kwargs['view_instance'].lookup_field = 'id'
self.kwargs['view_instance'].get_queryset = Mock(return_value=BitTestModel.objects.all())
self.kwargs['view_instance'].filter_queryset = lambda x: x.filter(is_active=True)
def test_should_use_view__get_queryset__and_filter_it_with__filter_queryset__and_filter_by__lookup_field(self):
model = BitTestModel.objects.create(is_active=True)
self.kwargs['view_instance'].kwargs = {'id': model.id}
expected = u"[(%s, True)]" % model.id
response = RetrieveModelKeyBit().get_data(**self.kwargs)
self.assertEqual(response, expected)
def test_with_bad_lookup_value(self):
self.kwargs['view_instance'].kwargs = {'id': "I'm ganna hack u are!"}
response = RetrieveModelKeyBit().get_data(**self.kwargs)
self.assertEqual(response, None)
def test_should_return_none_if_empty_queryset(self):
self.kwargs['view_instance'].filter_queryset = lambda x: x.none()
response = RetrieveModelKeyBit().get_data(**self.kwargs)
self.assertEqual(response, None)
def test_should_return_none_if_empty_result_set_raised(self):
self.kwargs['view_instance'].filter_queryset = lambda x: x.filter(pk__in=[])
response = RetrieveModelKeyBit().get_data(**self.kwargs)
self.assertEqual(response, None)
class ArgsKeyBitTest(TestCase):
def setUp(self):
self.test_args = ['abc', 'foobar', 'xyz']
self.kwargs = {
'params': None,
'view_instance': None,
'view_method': None,
'request': None,
'args': self.test_args,
'kwargs': None
}
def test_with_no_args(self):
self.assertEqual(ArgsKeyBit().get_data(**self.kwargs), [])
def test_with_all_args(self):
self.kwargs['params'] = '*'
self.assertEqual(ArgsKeyBit().get_data(**self.kwargs), self.test_args)
def test_with_specified_args(self):
self.kwargs['params'] = test_arg_idx = [0, 2]
expected_args = [self.test_args[i] for i in test_arg_idx]
self.assertEqual(ArgsKeyBit().get_data(**self.kwargs), expected_args)
def test_default_params_is_all_args(self):
self.assertEqual(ArgsKeyBit().params, '*')
class KwargsKeyBitTest(TestCase):
def setUp(self):
self.test_kwargs = {
'one': '1',
'city': 'London',
}
self.kwargs = {
'params': None,
'view_instance': None,
'view_method': None,
'request': None,
'args': None,
'kwargs': self.test_kwargs,
}
def test_resulting_dict_all_kwargs(self):
self.kwargs['params'] = '*'
self.assertEqual(KwargsKeyBit().get_data(**self.kwargs), self.test_kwargs)
def test_resulting_dict_specified_kwargs(self):
keys = ['one', 'not_existing_param']
expected_kwargs = {'one': self.test_kwargs['one']}
self.kwargs['params'] = keys
self.assertEqual(KwargsKeyBit().get_data(**self.kwargs), expected_kwargs)
def test_resulting_dict_no_kwargs(self):
self.assertEqual(KwargsKeyBit().get_data(**self.kwargs), {})
def test_default_params_is_all_args(self):
self.assertEqual(KwargsKeyBit().params, '*')
drf-extensions-0.7.1/tests_app/tests/unit/key_constructor/constructor/ 0000775 0000000 0000000 00000000000 14100726230 0026427 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/unit/key_constructor/constructor/__init__.py 0000664 0000000 0000000 00000000001 14100726230 0030527 0 ustar 00root root 0000000 0000000
drf-extensions-0.7.1/tests_app/tests/unit/key_constructor/constructor/tests.py 0000664 0000000 0000000 00000025450 14100726230 0030151 0 ustar 00root root 0000000 0000000 from copy import deepcopy
import hashlib
import json
from mock import Mock, patch
from django.test import TestCase
from rest_framework import viewsets
from rest_framework_extensions.key_constructor.constructors import (
KeyConstructor,
)
from rest_framework_extensions.utils import get_unique_method_id
from rest_framework.test import APIRequestFactory
from tests_app.testutils import (
override_extensions_api_settings,
TestFormatKeyBit,
TestUsedKwargsKeyBit,
TestLanguageKeyBit,
)
factory = APIRequestFactory()
class KeyConstructorTest_bits(TestCase):
def test_me(self):
class MyKeyConstructor(KeyConstructor):
format = TestFormatKeyBit()
language = TestLanguageKeyBit()
constructor_instance = MyKeyConstructor()
expected = {
'format': MyKeyConstructor.format,
'language': MyKeyConstructor.language
}
self.assertEqual(constructor_instance.bits, expected)
def test_with_inheritance(self):
class MyKeyConstructor(KeyConstructor):
format = TestFormatKeyBit()
class ChildKeyConstructor(MyKeyConstructor):
language = TestLanguageKeyBit()
constructor_instance = ChildKeyConstructor()
expected = {
'format': ChildKeyConstructor.format,
'language': ChildKeyConstructor.language
}
self.assertEqual(constructor_instance.bits, expected)
class KeyConstructorTest(TestCase):
def setUp(self):
class View(viewsets.ReadOnlyModelViewSet):
pass
view_intance = View()
view_method = view_intance.retrieve
self.kwargs = {
'view_instance': view_intance,
'view_method': view_method,
'request': factory.get(''),
'args': None,
'kwargs': None
}
def test_prepare_key_consistency_for_equal_dicts_with_different_key_positions(self):
class MyKeyConstructor(KeyConstructor):
pass
constructor_instance = MyKeyConstructor()
one = {'used_kwargs': {'view_method': 'view_method', 'kwargs': None, 'view_instance': 'view_instance', 'params': {'hello': 'world'}, 'args': None, 'request': 'request'}}
two = {'used_kwargs': {'params': {'hello': 'world'}, 'kwargs': None, 'view_method': 'view_method', 'view_instance': 'view_instance', 'args': None, 'request': 'request'}}
self.assertEqual(one, two)
self.assertEqual(constructor_instance.prepare_key(one), constructor_instance.prepare_key(two))
def prepare_key(self, key_dict):
return hashlib.md5(json.dumps(key_dict, sort_keys=True).encode('utf-8')).hexdigest()
def test_key_construction__with_bits_without_params(self):
class MyKeyConstructor(KeyConstructor):
format = TestFormatKeyBit()
language = TestLanguageKeyBit()
constructor_instance = MyKeyConstructor()
response = constructor_instance(**self.kwargs)
expected = {
'format': u'json',
'language': u'ru',
}
self.assertEqual(response, self.prepare_key(expected))
def test_key_construction__with_bits_with_params(self):
class MyKeyConstructor(KeyConstructor):
used_kwargs = TestUsedKwargsKeyBit(params={'hello': 'world'})
with patch.object(json.JSONEncoder, 'default', Mock(return_value='force serializing')):
constructor_instance = MyKeyConstructor()
response = constructor_instance(**self.kwargs)
expected_value = deepcopy(self.kwargs)
expected_value['params'] = {'hello': 'world'}
expected_data_from_bits = {
'used_kwargs': expected_value
}
msg = 'Data from bits: {data_from_bits}\nExpected data from: {expected_data_from_bits}'.format(
data_from_bits=json.dumps(constructor_instance.get_data_from_bits(**self.kwargs)),
expected_data_from_bits=json.dumps(expected_data_from_bits)
)
self.assertEqual(response, self.prepare_key(expected_data_from_bits), msg=msg)
def test_two_key_construction_with_same_bits_in_different_order_should_produce_equal_keys(self):
class MyKeyConstructor_1(KeyConstructor):
language = TestLanguageKeyBit()
format = TestFormatKeyBit()
class MyKeyConstructor_2(KeyConstructor):
format = TestFormatKeyBit()
language = TestLanguageKeyBit()
self.assertEqual(
MyKeyConstructor_1()(**self.kwargs),
MyKeyConstructor_2()(**self.kwargs)
)
def test_key_construction__with_bits_with_params__and_with_constructor_with_params(self):
class MyKeyConstructor(KeyConstructor):
used_kwargs = TestUsedKwargsKeyBit(params={'hello': 'world'})
with patch.object(json.JSONEncoder, 'default', Mock(return_value='force serializing')):
constructor_instance = MyKeyConstructor(params={
'used_kwargs': {'goodbye': 'moon'}
})
response = constructor_instance(**self.kwargs)
expected_value = deepcopy(self.kwargs)
expected_value['params'] = {'goodbye': 'moon'}
expected_data_from_bits = {
'used_kwargs': expected_value
}
msg = 'Data from bits: {data_from_bits}\nExpected data from: {expected_data_from_bits}'.format(
data_from_bits=json.dumps(constructor_instance.get_data_from_bits(**self.kwargs)),
expected_data_from_bits=json.dumps(expected_data_from_bits)
)
self.assertEqual(response, self.prepare_key(expected_data_from_bits), msg=msg)
class KeyConstructorTest___get_memoization_key(TestCase):
def setUp(self):
class View(viewsets.ReadOnlyModelViewSet):
pass
self.view_intance = View()
self.view_method = self.view_intance.retrieve
self.request = factory.get('')
def test_me(self):
class MyKeyConstructor(KeyConstructor):
language = TestLanguageKeyBit()
format = TestFormatKeyBit()
constructor_instance = MyKeyConstructor()
response = constructor_instance._get_memoization_key(
view_instance=self.view_intance,
view_method=self.view_method,
args=[1, 2, 3, u'Привет мир'],
kwargs={1: 2, 3: 4, u'привет': u'мир'}
)
expected = json.dumps({
'unique_method_id': get_unique_method_id(view_instance=self.view_intance, view_method=self.view_method),
'args': [1, 2, 3, u'Привет мир'],
'kwargs': {1: 2, 3: 4, u'привет': u'мир'},
'instance_id': id(constructor_instance)
})
self.assertEqual(response, expected)
class KeyConstructorTestBehavior__memoization(TestCase):
def setUp(self):
class View(viewsets.ReadOnlyModelViewSet):
pass
class MyKeyConstructor(KeyConstructor):
format = TestFormatKeyBit()
language = TestLanguageKeyBit()
view_intance = View()
view_method = view_intance.retrieve
self.MyKeyConstructor = MyKeyConstructor
self.kwargs = {
'view_instance': view_intance,
'view_method': view_method,
'request': factory.get(''),
'args': None,
'kwargs': None
}
def test_should_not_memoize_by_default(self):
constructor_instance = self.MyKeyConstructor()
response_1 = constructor_instance(**self.kwargs)
response_2 = constructor_instance(**self.kwargs)
self.assertFalse(response_1 is response_2)
def test_should_memoize_if_asked(self):
constructor_instance = self.MyKeyConstructor(memoize_for_request=True)
response_1 = constructor_instance(**self.kwargs)
response_2 = constructor_instance(**self.kwargs)
self.assertTrue(response_1 is response_2)
def test_should_use_value_from_settings_for_default_memoize_boolean_value(self):
with override_extensions_api_settings(DEFAULT_KEY_CONSTRUCTOR_MEMOIZE_FOR_REQUEST=True):
constructor_instance = KeyConstructor()
self.assertTrue(constructor_instance.memoize_for_request)
with override_extensions_api_settings(DEFAULT_KEY_CONSTRUCTOR_MEMOIZE_FOR_REQUEST=False):
constructor_instance = KeyConstructor()
self.assertFalse(constructor_instance.memoize_for_request)
def test_should_memoize_in_request_instance(self):
constructor_instance = self.MyKeyConstructor(memoize_for_request=True)
response_1 = constructor_instance(**self.kwargs)
self.kwargs['request'] = factory.get('')
response_2 = constructor_instance(**self.kwargs)
self.assertFalse(response_1 is response_2)
def test_should_use_different_memoization_for_different_arguments(self):
constructor_instance = self.MyKeyConstructor(memoize_for_request=True)
response_1 = constructor_instance(**self.kwargs)
response_2 = constructor_instance(**self.kwargs)
self.assertTrue(response_1 is response_2)
self.kwargs['args'] = [1, 2, 3, u'привет мир']
response_3 = constructor_instance(**self.kwargs)
response_4 = constructor_instance(**self.kwargs)
self.assertTrue(response_3 is response_4)
self.assertFalse(response_1 is response_3)
self.assertFalse(response_1 is response_4)
self.assertFalse(response_2 is response_3)
self.assertFalse(response_2 is response_4)
def test_should_use_different_memoization_for_different_request_instances(self):
constructor_instance = self.MyKeyConstructor(memoize_for_request=True)
response_1 = constructor_instance(**self.kwargs)
self.kwargs['request'] = factory.get('')
response_2 = constructor_instance(**self.kwargs)
self.assertFalse(response_1 is response_2)
def test_should_use_different_memoization_for_different_constructor_instances(self):
constructor_instance_1 = self.MyKeyConstructor(memoize_for_request=True)
constructor_instance_2 = self.MyKeyConstructor(memoize_for_request=True)
response_1 = constructor_instance_1(**self.kwargs)
response_2 = constructor_instance_2(**self.kwargs)
self.assertFalse(response_1 is response_2)
def test_should_use_different_memoization_for_different_views_with_same_method(self):
class View_2(viewsets.ReadOnlyModelViewSet):
pass
constructor_instance = self.MyKeyConstructor(memoize_for_request=True)
response_1 = constructor_instance(**self.kwargs)
view_2_instance = View_2()
self.kwargs['view_instance'] = view_2_instance
self.kwargs['view_instance'] = view_2_instance.retrieve
response_2 = constructor_instance(**self.kwargs)
self.assertFalse(response_1 is response_2)
drf-extensions-0.7.1/tests_app/tests/unit/migrations/ 0000775 0000000 0000000 00000000000 14100726230 0022761 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/unit/migrations/0001_initial.py 0000664 0000000 0000000 00000005421 14100726230 0025426 0 ustar 00root root 0000000 0000000 # Generated by Django 2.2 on 2019-04-16 11:51
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='BitTestModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_active', models.BooleanField(default=False)),
],
),
migrations.CreateModel(
name='NestedRouterMixinGroupModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=10)),
],
),
migrations.CreateModel(
name='NestedRouterMixinPermissionModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=10)),
],
),
migrations.CreateModel(
name='UserModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=20)),
],
),
migrations.CreateModel(
name='NestedRouterMixinUserModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=10)),
('groups', models.ManyToManyField(related_name='user_groups', to='unit.NestedRouterMixinGroupModel')),
],
),
migrations.AddField(
model_name='nestedroutermixingroupmodel',
name='permissions',
field=models.ManyToManyField(to='unit.NestedRouterMixinPermissionModel'),
),
migrations.CreateModel(
name='CommentModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=20)),
('text', models.CharField(max_length=200)),
('attachment', models.FileField(blank=True, max_length=500, null=True, upload_to='test_serializers')),
('hidden_text', models.CharField(blank=True, max_length=200, null=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='unit.UserModel')),
('users_liked', models.ManyToManyField(blank=True, to='unit.UserModel')),
],
),
]
drf-extensions-0.7.1/tests_app/tests/unit/migrations/__init__.py 0000664 0000000 0000000 00000000000 14100726230 0025060 0 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/unit/models.py 0000664 0000000 0000000 00000000177 14100726230 0022447 0 ustar 00root root 0000000 0000000 from .key_constructor.bits.models import *
from .routers.nested_router_mixin.models import *
from .serializers.models import *
drf-extensions-0.7.1/tests_app/tests/unit/routers/ 0000775 0000000 0000000 00000000000 14100726230 0022310 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/unit/routers/__init__.py 0000664 0000000 0000000 00000000000 14100726230 0024407 0 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/unit/routers/nested_router_mixin/ 0000775 0000000 0000000 00000000000 14100726230 0026376 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/unit/routers/nested_router_mixin/__init__.py 0000664 0000000 0000000 00000000001 14100726230 0030476 0 ustar 00root root 0000000 0000000
drf-extensions-0.7.1/tests_app/tests/unit/routers/nested_router_mixin/models.py 0000664 0000000 0000000 00000000771 14100726230 0030240 0 ustar 00root root 0000000 0000000 from django.db import models
class NestedRouterMixinPermissionModel(models.Model):
name = models.CharField(max_length=10)
class NestedRouterMixinGroupModel(models.Model):
name = models.CharField(max_length=10)
permissions = models.ManyToManyField(
'NestedRouterMixinPermissionModel')
class NestedRouterMixinUserModel(models.Model):
name = models.CharField(max_length=10)
groups = models.ManyToManyField(
'NestedRouterMixinGroupModel', related_name='user_groups')
drf-extensions-0.7.1/tests_app/tests/unit/routers/nested_router_mixin/tests.py 0000664 0000000 0000000 00000020210 14100726230 0030105 0 ustar 00root root 0000000 0000000 from rest_framework.test import APITestCase
from rest_framework_extensions.routers import ExtendedSimpleRouter
from rest_framework_extensions.utils import compose_parent_pk_kwarg_name
from .views import (
UserViewSet,
GroupViewSet,
PermissionViewSet,
CustomRegexUserViewSet,
CustomRegexGroupViewSet,
CustomRegexPermissionViewSet,
)
def get_regex_pattern(urlpattern):
return urlpattern.pattern.regex.pattern
class NestedRouterMixinTest(APITestCase):
def get_lookup_regex(self, value):
return '(?P<{0}>[^/.]+)'.format(value)
def get_parent_lookup_regex(self, value):
return '(?P<{0}>[^/.]+)'.format(compose_parent_pk_kwarg_name(value))
def get_custom_regex_lookup(self, pk_kwarg_name, lookup_value_regex):
""" Build lookup regex with custom regular expression. """
return '(?P<{pk_kwarg_name}>{lookup_value_regex})'.format(
pk_kwarg_name=pk_kwarg_name,
lookup_value_regex=lookup_value_regex
)
def get_custom_regex_parent_lookup(self, parent_pk_kwarg_name,
parent_lookup_value_regex):
""" Build parent lookup regex with custom regular expression. """
return self.get_custom_regex_lookup(
compose_parent_pk_kwarg_name(parent_pk_kwarg_name),
parent_lookup_value_regex
)
def test_one_route(self):
router = ExtendedSimpleRouter()
router.register(r'users', UserViewSet, 'user')
# test user list
self.assertEqual(router.urls[0].name, 'user-list')
self.assertEqual(get_regex_pattern(router.urls[0]), r'^users/$')
# test user detail
self.assertEqual(router.urls[1].name, 'user-detail')
self.assertEqual(get_regex_pattern(router.urls[1]), r'^users/{0}/$'.format(self.get_lookup_regex('pk')))
def test_nested_route(self):
router = ExtendedSimpleRouter()
(
router.register(r'users', UserViewSet, 'user')
.register(r'groups', GroupViewSet, 'users-group', parents_query_lookups=['user'])
)
# test user list
self.assertEqual(router.urls[0].name, 'user-list')
self.assertEqual(get_regex_pattern(router.urls[0]), r'^users/$')
# test user detail
self.assertEqual(router.urls[1].name, 'user-detail')
self.assertEqual(get_regex_pattern(router.urls[1]), r'^users/{0}/$'.format(self.get_lookup_regex('pk')))
# test users group list
self.assertEqual(router.urls[2].name, 'users-group-list')
self.assertEqual(get_regex_pattern(router.urls[2]), r'^users/{0}/groups/$'.format(
self.get_parent_lookup_regex('user')
)
)
# test users group detail
self.assertEqual(router.urls[3].name, 'users-group-detail')
self.assertEqual(get_regex_pattern(router.urls[3]), r'^users/{0}/groups/{1}/$'.format(
self.get_parent_lookup_regex('user'),
self.get_lookup_regex('pk')
),
)
def test_nested_route_depth_3(self):
router = ExtendedSimpleRouter()
(
router.register(r'users', UserViewSet, 'user')
.register(r'groups', GroupViewSet, 'users-group', parents_query_lookups=['user'])
.register(r'permissions', PermissionViewSet, 'users-groups-permission', parents_query_lookups=[
'group__user',
'group',
]
)
)
# test user list
self.assertEqual(router.urls[0].name, 'user-list')
self.assertEqual(get_regex_pattern(router.urls[0]), r'^users/$')
# test user detail
self.assertEqual(router.urls[1].name, 'user-detail')
self.assertEqual(get_regex_pattern(router.urls[1]), r'^users/{0}/$'.format(self.get_lookup_regex('pk')))
# test users group list
self.assertEqual(router.urls[2].name, 'users-group-list')
self.assertEqual(get_regex_pattern(router.urls[2]), r'^users/{0}/groups/$'.format(
self.get_parent_lookup_regex('user')
)
)
# test users group detail
self.assertEqual(router.urls[3].name, 'users-group-detail')
self.assertEqual(get_regex_pattern(router.urls[3]), r'^users/{0}/groups/{1}/$'.format(
self.get_parent_lookup_regex('user'),
self.get_lookup_regex('pk')
),
)
# test users groups permission list
self.assertEqual(router.urls[4].name, 'users-groups-permission-list')
self.assertEqual(get_regex_pattern(router.urls[4]), r'^users/{0}/groups/{1}/permissions/$'.format(
self.get_parent_lookup_regex('group__user'),
self.get_parent_lookup_regex('group'),
)
)
# test users groups permission detail
self.assertEqual(router.urls[5].name, 'users-groups-permission-detail')
self.assertEqual(get_regex_pattern(router.urls[5]), r'^users/{0}/groups/{1}/permissions/{2}/$'.format(
self.get_parent_lookup_regex('group__user'),
self.get_parent_lookup_regex('group'),
self.get_lookup_regex('pk')
),
)
def test_nested_route_depth_3_custom_regex(self):
"""
Nested routes with over two level of depth should respect all parents'
`lookup_value_regex` attribute.
"""
router = ExtendedSimpleRouter()
(
router.register(r'users', CustomRegexUserViewSet, 'user')
.register(r'groups', CustomRegexGroupViewSet, 'users-group',
parents_query_lookups=['user'])
.register(r'permissions', CustomRegexPermissionViewSet,
'users-groups-permission', parents_query_lookups=[
'group__user',
'group',
]
)
)
# custom regex configuration
user_viewset_regex = CustomRegexUserViewSet.lookup_value_regex
group_viewset_regex = CustomRegexGroupViewSet.lookup_value_regex
perm_viewset_regex = CustomRegexPermissionViewSet.lookup_value_regex
# test user list
self.assertEqual(router.urls[0].name, 'user-list')
self.assertEqual(get_regex_pattern(router.urls[0]), r'^users/$')
# test user detail
self.assertEqual(router.urls[1].name, 'user-detail')
self.assertEqual(get_regex_pattern(router.urls[1]), r'^users/{0}/$'.format(
self.get_custom_regex_lookup('pk', user_viewset_regex))
)
# test users group list
self.assertEqual(router.urls[2].name, 'users-group-list')
self.assertEqual(get_regex_pattern(router.urls[2]), r'^users/{0}/groups/$'.format(
self.get_custom_regex_parent_lookup('user', user_viewset_regex)
)
)
# test users group detail
self.assertEqual(router.urls[3].name, 'users-group-detail')
self.assertEqual(get_regex_pattern(router.urls[3]), r'^users/{0}/groups/{1}/$'.format(
self.get_custom_regex_parent_lookup('user', user_viewset_regex),
self.get_custom_regex_lookup('pk', group_viewset_regex)
),
)
# test users groups permission list
self.assertEqual(router.urls[4].name, 'users-groups-permission-list')
self.assertEqual(get_regex_pattern(router.urls[4]), r'^users/{0}/groups/{1}/permissions/$'.format(
self.get_custom_regex_parent_lookup('group__user', user_viewset_regex),
self.get_custom_regex_parent_lookup('group', group_viewset_regex),
)
)
# test users groups permission detail
self.assertEqual(router.urls[5].name, 'users-groups-permission-detail')
self.assertEqual(get_regex_pattern(router.urls[5]), r'^users/{0}/groups/{1}/permissions/{2}/$'.format(
self.get_custom_regex_parent_lookup('group__user', user_viewset_regex),
self.get_custom_regex_parent_lookup('group', group_viewset_regex),
self.get_custom_regex_lookup('pk', perm_viewset_regex)
),
)
drf-extensions-0.7.1/tests_app/tests/unit/routers/nested_router_mixin/views.py 0000664 0000000 0000000 00000001311 14100726230 0030101 0 ustar 00root root 0000000 0000000 from rest_framework.viewsets import ModelViewSet
from .models import (
NestedRouterMixinUserModel as UserModel,
NestedRouterMixinGroupModel as GroupModel,
NestedRouterMixinPermissionModel as PermissionModel,
)
class UserViewSet(ModelViewSet):
model = UserModel
class GroupViewSet(ModelViewSet):
model = GroupModel
class PermissionViewSet(ModelViewSet):
model = PermissionModel
class CustomRegexUserViewSet(ModelViewSet):
lookup_value_regex = 'a'
model = UserModel
class CustomRegexGroupViewSet(ModelViewSet):
lookup_value_regex = 'b'
model = GroupModel
class CustomRegexPermissionViewSet(ModelViewSet):
lookup_value_regex = 'c'
model = PermissionModel
drf-extensions-0.7.1/tests_app/tests/unit/routers/tests.py 0000664 0000000 0000000 00000020472 14100726230 0024031 0 ustar 00root root 0000000 0000000 from django.test import TestCase
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework_extensions.routers import ExtendedDefaultRouter
class ExtendedDefaultRouterTest(TestCase):
def setUp(self):
self.router = ExtendedDefaultRouter()
def get_routes_names(self, routes):
return [i.name for i in routes]
def get_dynamic_route_by_def_name(self, def_name, routes):
try:
return [i for i in routes if def_name in i.mapping.values()][0]
except IndexError:
return None
def test_dynamic_list_route_should_come_before_detail_route(self):
class BasicViewSet(viewsets.ViewSet):
def list(self, request, *args, **kwargs):
return Response({'method': 'list'})
@action(detail=False)
def detail1(self, request, *args, **kwargs):
return Response({'method': 'detail1'})
routes = self.router.get_routes(BasicViewSet)
expected = [
'{basename}-list',
'{basename}-detail1',
'{basename}-detail'
]
msg = '@detail_route methods should come first in routes order'
self.assertEqual(self.get_routes_names(routes), expected, msg)
def test_detail_route(self):
class BasicViewSet(viewsets.ViewSet):
@action(detail=True)
def action1(self, request, *args, **kwargs):
pass
routes = self.router.get_routes(BasicViewSet)
action1_route = self.get_dynamic_route_by_def_name('action1', routes)
msg = '@detail_route should map methods to def name'
self.assertEqual(action1_route.mapping, {'get': 'action1'}, msg)
msg = '@detail_route should use url with detail lookup'
self.assertEqual(action1_route.url, u'^{prefix}/{lookup}/action1{trailing_slash}$', msg)
def test_detail_route__with_methods(self):
class BasicViewSet(viewsets.ViewSet):
@action(detail=True, methods=['post'])
def action1(self, request, *args, **kwargs):
pass
routes = self.router.get_routes(BasicViewSet)
action1_route = self.get_dynamic_route_by_def_name('action1', routes)
msg = '@detail_route should map methods to def name'
self.assertEqual(action1_route.mapping, {'post': 'action1'}, msg)
msg = '@detail_route should use url with detail lookup'
self.assertEqual(action1_route.url, u'^{prefix}/{lookup}/action1{trailing_slash}$', msg)
def test_detail_route__with_methods__and__with_url_path(self):
class BasicViewSet(viewsets.ViewSet):
@action(detail=True, methods=['post'], url_path='action-one')
def action1(self, request, *args, **kwargs):
pass
routes = self.router.get_routes(BasicViewSet)
action1_route = self.get_dynamic_route_by_def_name('action1', routes)
msg = '@detail_route should map methods to "url_path"'
self.assertEqual(action1_route.mapping, {'post': 'action1'}, msg)
msg = '@detail_route should use url with detail lookup and "url_path" value'
self.assertEqual(action1_route.url, u'^{prefix}/{lookup}/action-one{trailing_slash}$', msg)
def test_list_route(self):
class BasicViewSet(viewsets.ViewSet):
@action(detail=False)
def action1(self, request, *args, **kwargs):
pass
routes = self.router.get_routes(BasicViewSet)
action1_route = self.get_dynamic_route_by_def_name('action1', routes)
msg = '@list_route should map methods to def name'
self.assertEqual(action1_route.mapping, {'get': 'action1'}, msg)
msg = '@list_route should use url in list scope'
self.assertEqual(action1_route.url, u'^{prefix}/action1{trailing_slash}$', msg)
def test_list_route__with_methods(self):
class BasicViewSet(viewsets.ViewSet):
@action(detail=False, methods=['post'])
def action1(self, request, *args, **kwargs):
pass
routes = self.router.get_routes(BasicViewSet)
action1_route = self.get_dynamic_route_by_def_name('action1', routes)
msg = '@list_route should map methods to def name'
self.assertEqual(action1_route.mapping, {'post': 'action1'}, msg)
msg = '@list_route should use url in list scope'
self.assertEqual(action1_route.url, u'^{prefix}/action1{trailing_slash}$', msg)
def test_list_route__with_methods__and__with_url_path(self):
class BasicViewSet(viewsets.ViewSet):
@action(detail=False, methods=['post'], url_path='action-one')
def action1(self, request, *args, **kwargs):
pass
routes = self.router.get_routes(BasicViewSet)
action1_route = self.get_dynamic_route_by_def_name('action1', routes)
msg = '@list_route should map methods to "url_path"'
self.assertEqual(action1_route.mapping, {'post': 'action1'}, msg)
msg = '@list_route should use url in list scope with "url_path" value'
self.assertEqual(action1_route.url, u'^{prefix}/action-one{trailing_slash}$', msg)
def test_list_route_and_detail_route_with_exact_names(self):
class BasicViewSet(viewsets.ViewSet):
@action(detail=False, url_path='action-one')
def action1(self, request, *args, **kwargs):
pass
@action(detail=True, url_path='action-one')
def action1_detail(self, request, *args, **kwargs):
pass
routes = self.router.get_routes(BasicViewSet)
action1_list_route = self.get_dynamic_route_by_def_name('action1', routes)
action1_detail_route = self.get_dynamic_route_by_def_name('action1_detail', routes)
self.assertEqual(action1_list_route.mapping, {'get': 'action1'})
self.assertEqual(action1_list_route.url, u'^{prefix}/action-one{trailing_slash}$')
self.assertEqual(action1_detail_route.mapping, {'get': 'action1_detail'})
self.assertEqual(action1_detail_route.url, u'^{prefix}/{lookup}/action-one{trailing_slash}$')
def test_list_route_and_detail_route_names(self):
class BasicViewSet(viewsets.ViewSet):
@action(detail=False)
def action1(self, request, *args, **kwargs):
pass
@action(detail=True)
def action2(self, request, *args, **kwargs):
pass
routes = self.router.get_routes(BasicViewSet)
action1_list_route = self.get_dynamic_route_by_def_name('action1', routes)
action2_detail_route = self.get_dynamic_route_by_def_name('action2', routes)
self.assertEqual(action1_list_route.name, u'{basename}-action1')
self.assertEqual(action2_detail_route.name, u'{basename}-action2')
def test_list_route_and_detail_route_default_names__with_endpoints(self):
class BasicViewSet(viewsets.ViewSet):
@action(detail=False, url_path='action_one')
def action1(self, request, *args, **kwargs):
pass
@action(detail=True, url_path='action-two')
def action2(self, request, *args, **kwargs):
pass
routes = self.router.get_routes(BasicViewSet)
action1_list_route = self.get_dynamic_route_by_def_name('action1', routes)
action2_detail_route = self.get_dynamic_route_by_def_name('action2', routes)
self.assertEqual(action1_list_route.name, u'{basename}-action1')
self.assertEqual(action2_detail_route.name, u'{basename}-action2')
def test_list_route_and_detail_route_names__with_endpoints(self):
class BasicViewSet(viewsets.ViewSet):
@action(detail=False, url_path='action_one', url_name='action_one')
def action1(self, request, *args, **kwargs):
pass
@action(detail=True, url_path='action-two', url_name='action-two')
def action2(self, request, *args, **kwargs):
pass
routes = self.router.get_routes(BasicViewSet)
action1_list_route = self.get_dynamic_route_by_def_name('action1', routes)
action2_detail_route = self.get_dynamic_route_by_def_name('action2', routes)
self.assertEqual(action1_list_route.name, u'{basename}-action_one')
self.assertEqual(action2_detail_route.name, u'{basename}-action-two')
drf-extensions-0.7.1/tests_app/tests/unit/serializers/ 0000775 0000000 0000000 00000000000 14100726230 0023141 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/unit/serializers/__init__.py 0000664 0000000 0000000 00000000001 14100726230 0025241 0 ustar 00root root 0000000 0000000
drf-extensions-0.7.1/tests_app/tests/unit/serializers/models.py 0000664 0000000 0000000 00000001131 14100726230 0024772 0 ustar 00root root 0000000 0000000 from django.db import models
class UserModel(models.Model):
name = models.CharField(max_length=20)
class CommentModel(models.Model):
user = models.ForeignKey(
UserModel,
related_name='comments',
on_delete=models.CASCADE,
)
users_liked = models.ManyToManyField(UserModel, blank=True)
title = models.CharField(max_length=20)
text = models.CharField(max_length=200)
attachment = models.FileField(
upload_to='test_serializers', blank=True, null=True, max_length=500)
hidden_text = models.CharField(max_length=200, blank=True, null=True)
drf-extensions-0.7.1/tests_app/tests/unit/serializers/serializers.py 0000664 0000000 0000000 00000003620 14100726230 0026050 0 ustar 00root root 0000000 0000000 from rest_framework import serializers
from rest_framework_extensions import serializers as drf_serializers
from .models import CommentModel, UserModel
class UserSerializer(drf_serializers.PartialUpdateSerializerMixin,
serializers.ModelSerializer):
class Meta:
model = UserModel
fields = (
'name',
'comments'
)
class CommentSerializer(drf_serializers.PartialUpdateSerializerMixin,
serializers.ModelSerializer):
title_from_source = serializers.CharField(source='title', required=False)
class Meta:
model = CommentModel
fields = (
'id',
'user',
'users_liked',
'title',
'text',
'attachment',
'title_from_source'
)
class CommentTextSerializer(drf_serializers.PartialUpdateSerializerMixin,
serializers.ModelSerializer):
class Meta:
model = CommentModel
fields = (
'title',
'text'
)
class CommentSerializerWithGroupedFields(CommentSerializer):
text_content = CommentTextSerializer(source='*')
class Meta(CommentSerializer.Meta):
fields = (
'id',
'user',
'users_liked',
'attachment',
'title_from_source',
'text_content'
)
class CommentSerializerWithAllowedUserId(CommentSerializer):
user_id = serializers.IntegerField()
class Meta(CommentSerializer.Meta):
fields = ('user_id',) + CommentSerializer.Meta.fields
class CommentSerializerWithExpandedUsersLiked(drf_serializers.PartialUpdateSerializerMixin,
serializers.ModelSerializer):
user = UserSerializer()
class Meta:
model = CommentModel
fields = (
'title',
'user'
)
drf-extensions-0.7.1/tests_app/tests/unit/serializers/tests.py 0000664 0000000 0000000 00000025005 14100726230 0024657 0 ustar 00root root 0000000 0000000 from django.test import TestCase
from django.core.files.base import ContentFile
from rest_framework_extensions.serializers import get_fields_for_partial_update
from .serializers import CommentSerializer, UserSerializer, \
CommentSerializerWithGroupedFields, CommentSerializerWithAllowedUserId
from .models import UserModel, CommentModel
class PartialUpdateSerializerMixinTest(TestCase):
def setUp(self):
self.files = [
ContentFile(u'file one'.encode('utf-8'), name='file1.txt'),
ContentFile(u'file two'.encode('utf-8'), name='file2.txt'),
]
self.files[0].size = 8
self.files[1].size = 8
self.user = UserModel.objects.create(name='gena')
self.comment = CommentModel.objects.create(
user=self.user,
title='hello',
text='world',
attachment=self.files[0]
)
def get_comment(self):
return CommentModel.objects.get(pk=self.comment.pk)
def test_should_use_default_saving_without_partial(self):
serializer = CommentSerializer(data={
'user': self.user.id,
'title': 'hola',
'text': 'amigos',
})
self.assertTrue(serializer.is_valid()) # bug for python3 comes from here
saved_object = serializer.save()
self.assertEqual(saved_object.user, self.user)
self.assertEqual(saved_object.title, 'hola')
self.assertEqual(saved_object.text, 'amigos')
def test_should_save_partial(self):
serializer = CommentSerializer(
instance=self.comment, data={'title': 'hola'}, partial=True)
self.assertTrue(serializer.is_valid())
saved_object = serializer.save()
self.assertEqual(saved_object.user, self.user)
self.assertEqual(saved_object.title, 'hola')
self.assertEqual(saved_object.text, 'world')
def test_update_fields_correctly_determined(self):
serializer = CommentSerializerWithGroupedFields(
instance=self.comment,
data={'text_content': {'title': 'hola', 'text': 'group'},
'title_from_source': 'hola', 'attachment': self.files[1]},
partial=True)
update_fields = get_fields_for_partial_update(
serializer.Meta,
serializer.get_initial(),
serializer.fields.fields)
self.assertListEqual(update_fields, ['attachment', 'text', 'title'])
def test_should_save_grouped_partial(self):
serializer = CommentSerializerWithGroupedFields(
instance=self.comment,
data={'text_content': {'title': 'hola', 'text': 'group'}},
partial=True)
self.assertTrue(serializer.is_valid())
serializer.save()
self.comment.refresh_from_db()
self.assertEqual(self.comment.user, self.user)
self.assertEqual(self.comment.title, 'hola')
self.assertEqual(self.comment.text, 'group')
def test_should_save_only_fields_from_data_for_partial_update(self):
# it's important to use different instances for Comment,
# because serializer's save method affects instance from arguments
serializer_one = CommentSerializer(
instance=self.get_comment(),
data={'title': 'goodbye'}, partial=True)
serializer_two = CommentSerializer(
instance=self.get_comment(), data={'text': 'moon'}, partial=True)
serializer_three_kwargs = {
'instance': self.get_comment(),
'partial': True
}
serializer_three_kwargs['data'] = {'attachment': self.files[1]}
serializer_three = CommentSerializer(**serializer_three_kwargs)
self.assertTrue(serializer_one.is_valid())
self.assertTrue(serializer_two.is_valid())
self.assertTrue(serializer_three.is_valid())
# saving three serializers expecting they don't affect each other's saving
serializer_one.save()
serializer_two.save()
serializer_three.save()
fresh_instance = self.get_comment()
self.assertEqual(fresh_instance.attachment.read(), u'file two'.encode('utf-8'))
fresh_instance.attachment.close()
self.assertEqual(fresh_instance.text, 'moon')
self.assertEqual(fresh_instance.title, 'goodbye')
def test_should_use_related_field_name_for_update_field_list(self):
another_user = UserModel.objects.create(name='vova')
data = {
'title': 'goodbye',
'user': another_user.pk
}
serializer = CommentSerializer(
instance=self.get_comment(), data=data, partial=True)
self.assertTrue(serializer.is_valid())
serializer.save()
fresh_instance = self.get_comment()
self.assertEqual(fresh_instance.title, 'goodbye')
self.assertEqual(fresh_instance.user, another_user)
def test_should_use_field_source_value_for_searching_model_concrete_fields(self):
data = {
'title_from_source': 'goodbye'
}
serializer = CommentSerializer(
instance=self.get_comment(), data=data, partial=True)
self.assertTrue(serializer.is_valid())
serializer.save()
fresh_instance = self.get_comment()
self.assertEqual(fresh_instance.title, 'goodbye')
def test_should_not_use_m2m_field_name_for_update_field_list(self):
another_user = UserModel.objects.create(name='vova')
data = {
'title': 'goodbye',
'users_liked': [self.user.pk, another_user.pk]
}
serializer = CommentSerializer(
instance=self.get_comment(), data=data, partial=True)
self.assertTrue(serializer.is_valid())
try:
serializer.save()
except ValueError:
self.fail(
'If m2m field used in partial update then it should not be used in update_fields list')
fresh_instance = self.get_comment()
self.assertEqual(fresh_instance.title, 'goodbye')
users_liked = set(
fresh_instance.users_liked.all().values_list('pk', flat=True))
self.assertEqual(
users_liked, set([self.user.pk, another_user.pk]))
def test_should_not_use_related_set_field_name_for_update_field_list(self):
another_user = UserModel.objects.create(name='vova')
another_comment = CommentModel.objects.create(
user=another_user,
title='goodbye',
text='moon',
)
data = {
'name': 'vova',
'comments': [another_comment.pk]
}
serializer = UserSerializer(instance=another_user, data=data, partial=True)
self.assertTrue(serializer.is_valid())
serializer.save()
try:
serializer.save()
except ValueError:
self.fail('If related set field used in partial update then it should not be used in update_fields list')
fresh_comment = CommentModel.objects.get(pk=another_comment.pk)
fresh_user = UserModel.objects.get(pk=another_user.pk)
self.assertEqual(fresh_comment.user, another_user)
self.assertEqual(fresh_user.name, 'vova')
def test_should_not_try_to_update_fields_that_are_not_in_model(self):
data = {
'title': 'goodbye',
'not_existing_field': 'moon'
}
serializer = CommentSerializer(instance=self.get_comment(), data=data, partial=True)
self.assertTrue(serializer.is_valid())
try:
serializer.save()
except ValueError:
msg = 'Should not pass values to update_fields from data, if they are not in model'
self.fail(msg)
fresh_instance = self.get_comment()
self.assertEqual(fresh_instance.title, 'goodbye')
self.assertEqual(fresh_instance.text, 'world')
def test_should_not_try_to_update_fields_that_are_not_allowed_from_serializer(self):
data = {
'title': 'goodbye',
'hidden_text': 'do not change me'
}
serializer = CommentSerializer(instance=self.get_comment(), data=data, partial=True)
self.assertTrue(serializer.is_valid())
serializer.save()
fresh_instance = self.get_comment()
self.assertEqual(fresh_instance.title, 'goodbye')
self.assertEqual(fresh_instance.text, 'world')
self.assertEqual(fresh_instance.hidden_text, None)
def test_should_use_list_of_fields_to_update_from_arguments_if_it_passed(self):
data = {
'title': 'goodbye',
'text': 'moon'
}
serializer = CommentSerializer(instance=self.get_comment(), data=data, partial=True)
self.assertTrue(serializer.is_valid())
serializer.save(**{'update_fields': ['title']})
fresh_instance = self.get_comment()
self.assertEqual(fresh_instance.title, 'goodbye')
self.assertEqual(fresh_instance.text, 'world')
def test_should_not_use_field_attname_for_update_fields__if_attname_not_allowed_in_serializer_fields(self):
another_user = UserModel.objects.create(name='vova')
data = {
'title': 'goodbye',
'user_id': another_user.id
}
serializer = CommentSerializer(
instance=self.get_comment(), data=data, partial=True)
self.assertTrue(serializer.is_valid())
serializer.save()
fresh_instance = self.get_comment()
self.assertEqual(fresh_instance.user_id, self.user.id)
def test_should_use_field_attname_for_update_fields__if_attname_allowed_in_serializer_fields(self):
another_user = UserModel.objects.create(name='vova')
data = {
'title': 'goodbye',
'user_id': another_user.id
}
serializer = CommentSerializerWithAllowedUserId(
instance=self.get_comment(), data=data, partial=True)
self.assertTrue(serializer.is_valid())
serializer.save()
fresh_instance = self.get_comment()
self.assertEqual(fresh_instance.user_id, another_user.id)
def test_should_not_use_pk_field_for_update_fields(self):
old_pk = self.get_comment().pk
data = {
'id': old_pk + 1,
'title': 'goodbye'
}
serializer = CommentSerializer(
instance=self.get_comment(), data=data, partial=True)
self.assertTrue(serializer.is_valid())
try:
serializer.save()
except ValueError:
self.fail(
'Primary key field should be excluded from update_fields list')
fresh_instance = self.get_comment()
self.assertEqual(fresh_instance.pk, old_pk)
self.assertEqual(fresh_instance.title, u'goodbye')
drf-extensions-0.7.1/tests_app/tests/unit/utils/ 0000775 0000000 0000000 00000000000 14100726230 0021745 5 ustar 00root root 0000000 0000000 drf-extensions-0.7.1/tests_app/tests/unit/utils/__init__.py 0000664 0000000 0000000 00000000001 14100726230 0024045 0 ustar 00root root 0000000 0000000
drf-extensions-0.7.1/tests_app/tests/unit/utils/tests.py 0000664 0000000 0000000 00000002653 14100726230 0023467 0 ustar 00root root 0000000 0000000 import contextlib
try:
from unittest import mock
except ImportError:
import mock
from django.test import TestCase
from rest_framework_extensions.utils import prepare_header_name, get_rest_framework_version
@contextlib.contextmanager
def parsed_version(version):
with mock.patch('rest_framework.VERSION', version):
yield get_rest_framework_version()
class TestPrepareHeaderName(TestCase):
def test_upper(self):
self.assertEqual(prepare_header_name('Accept'), 'HTTP_ACCEPT')
def test_replace_dash_with_underscores(self):
self.assertEqual(
prepare_header_name('Accept-Language'), 'HTTP_ACCEPT_LANGUAGE')
def test_strips_whitespaces(self):
self.assertEqual(
prepare_header_name(' Accept-Language '), 'HTTP_ACCEPT_LANGUAGE')
def test_adds_http_prefix(self):
self.assertEqual(
prepare_header_name('Accept-Language'), 'HTTP_ACCEPT_LANGUAGE')
def test_get_rest_framework_version_exotic_version(self):
"""See """
with parsed_version('1.2alphaSOMETHING') as version:
self.assertEqual(version, (1, 2, 'alpha', 'SOMETHING'))
def test_get_rest_framework_version_normal_version(self):
"""See """
with parsed_version('3.14.16') as version:
self.assertEqual(version, (3, 14, 16))
drf-extensions-0.7.1/tests_app/testutils.py 0000664 0000000 0000000 00000002401 14100726230 0021073 0 ustar 00root root 0000000 0000000 import base64
from mock import patch
from rest_framework import HTTP_HEADER_ENCODING
from rest_framework_extensions.key_constructor import bits
from rest_framework_extensions.key_constructor.constructors import KeyConstructor
def get_url_pattern_by_regex_pattern(patterns, pattern_string):
# todo: test me
for pattern in patterns:
if pattern.pattern.regex.pattern == pattern_string:
return pattern
def override_extensions_api_settings(**kwargs):
return patch.multiple(
'rest_framework_extensions.settings.extensions_api_settings',
**kwargs
)
def basic_auth_header(username, password):
credentials = ('%s:%s' % (username, password))
base64_credentials = base64.b64encode(
credentials.encode(HTTP_HEADER_ENCODING)
).decode(HTTP_HEADER_ENCODING)
return 'Basic %s' % base64_credentials
class TestFormatKeyBit(bits.KeyBitBase):
def get_data(self, **kwargs):
return u'json'
class TestLanguageKeyBit(bits.KeyBitBase):
def get_data(self, **kwargs):
return u'ru'
class TestUsedKwargsKeyBit(bits.KeyBitBase):
def get_data(self, **kwargs):
return kwargs
class TestKeyConstructor(KeyConstructor):
format = TestFormatKeyBit()
language = TestLanguageKeyBit()
drf-extensions-0.7.1/tests_app/urls.py 0000664 0000000 0000000 00000000021 14100726230 0020014 0 ustar 00root root 0000000 0000000 urlpatterns = []
drf-extensions-0.7.1/tox.ini 0000664 0000000 0000000 00000001627 14100726230 0016003 0 ustar 00root root 0000000 0000000 [tox]
envlist = py{36,37,38}-django{22}-drf{39,310,311,312}
py{36,37,38}-django{30}-drf{310,311,312}
py{36,37,38}-django{31}-drf{311,312}
py{36,37,38}-django{32}-drf{312}
[testenv]
deps=
-rtests_app/requirements.txt
django-guardian>=1.4.4
drf39: djangorestframework>=3.9.3,<3.10
djangorestframework-guardian
drf310: djangorestframework>=3.10,<3.11
djangorestframework-guardian
drf311: djangorestframework>=3.11,<3.12
djangorestframework-guardian
drf312: djangorestframework>=3.12,<3.13
djangorestframework-guardian
django22: Django>=2.2,<3.0
django30: Django>=3.0,<3.1
django31: Django>=3.1,<3.2
django32: Django>=3.2
setenv =
PYTHONPATH = {toxinidir}:{toxinidir}/tests_app
commands =
python --version
pip freeze
python -Wd {envbindir}/django-admin.py test --settings=settings {posargs}